# Local development stack for the finance MVP monorepo. # # Usage: # cp .env.example .env # from infra/ # docker compose up -d --wait # full stack (requires backend/ and frontend/ to exist) # docker compose up -d --wait postgres redis minio mailpit # infra-only, before app code lands # # Production runs the same provider-neutral images (infra/docker/) on a hosting # platform that supplies PostgreSQL, Redis, S3-compatible storage, SMTP and a # secret manager located in the Russian Federation. See docs/adr/0004-*.md and # docs/operations/beta-release-runbook.md. name: finance x-backend-build: &backend-build context: ../backend dockerfile: ../infra/docker/Dockerfile.backend x-api-env: &api-env ENVIRONMENT: ${APP_ENV:-development} TZ: ${TZ:-Europe/Moscow} COOKIE_SECURE: "false" # Application-level configuration. Postgres/Redis/S3/SMTP credentials are # constructed from the infra services below; production overrides come from # the platform secret manager only. DATABASE_URL: postgresql+psycopg://${APP_DATABASE_USER:?APP_DATABASE_USER is required}:${APP_DATABASE_PASSWORD:?APP_DATABASE_PASSWORD is required}@postgres:5432/${POSTGRES_DB:-finance} REDIS_URL: redis://redis:6379/0 CELERY_BROKER_URL: redis://redis:6379/0 S3_ENDPOINT_URL: http://minio:9000 S3_USE_SSL: "false" S3_REGION: ${S3_REGION:-us-east-1} S3_ACCESS_KEY_ID: ${MINIO_ROOT_USER:?MINIO_ROOT_USER is required} S3_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required} S3_BUCKET_STATEMENTS: ${S3_BUCKET_STATEMENTS:-finance-statements} SMTP_HOST: mailpit SMTP_PORT: "1025" SMTP_USE_TLS: "false" SMTP_FROM: ${SMTP_FROM:-no-reply@finance.local} PUBLIC_BASE_URL: http://localhost:${FRONTEND_HOST_PORT:-8080} SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET is required} DATA_ENCRYPTION_KEK_ID: ${DATA_ENCRYPTION_KEK_ID:-dev-kek-1} DATA_ENCRYPTION_KEK: ${DATA_ENCRYPTION_KEK:?DATA_ENCRYPTION_KEK is required} IMPORTS_KEK_B64: ${DATA_ENCRYPTION_KEK:?DATA_ENCRYPTION_KEK is required} ADMIN_BOOTSTRAP_TOKEN: ${ADMIN_BOOTSTRAP_TOKEN:?ADMIN_BOOTSTRAP_TOKEN is required} services: postgres: image: postgres:17-alpine restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER:?POSTGRES_USER is required} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required} POSTGRES_DB: ${POSTGRES_DB:-finance} APP_DATABASE_USER: ${APP_DATABASE_USER:?APP_DATABASE_USER is required} APP_DATABASE_PASSWORD: ${APP_DATABASE_PASSWORD:?APP_DATABASE_PASSWORD is required} TZ: ${TZ:-Europe/Moscow} ports: - "127.0.0.1:${POSTGRES_HOST_PORT:-5432}:5432" volumes: - postgres_data:/var/lib/postgresql/data - ./postgres/init-app-role.sh:/docker-entrypoint-initdb.d/10-app-role.sh:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 5s timeout: 3s retries: 12 start_period: 10s networks: [backend] redis: image: redis:7-alpine restart: unless-stopped command: ["redis-server", "--appendonly", "yes"] ports: - "127.0.0.1:${REDIS_HOST_PORT:-6379}:6379" volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 12 start_period: 5s networks: [backend] minio: image: quay.io/minio/minio:latest restart: unless-stopped command: ["server", "/data", "--console-address", ":9001"] environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER:?MINIO_ROOT_USER is required} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required} TZ: ${TZ:-Europe/Moscow} ports: - "127.0.0.1:${MINIO_API_HOST_PORT:-9000}:9000" - "127.0.0.1:${MINIO_CONSOLE_HOST_PORT:-9001}:9001" volumes: - minio_data:/data healthcheck: test: ["CMD", "mc", "ready", "local"] interval: 5s timeout: 5s retries: 12 start_period: 10s networks: [backend, edge] # Test SMTP sink. Captures all outbound mail; UI at http://127.0.0.1:8025. mailpit: image: axllent/mailpit:latest restart: unless-stopped environment: MP_DATA_FILE: /data/mailpit.db TZ: ${TZ:-Europe/Moscow} ports: - "127.0.0.1:${MAILPIT_SMTP_HOST_PORT:-1025}:1025" - "127.0.0.1:${MAILPIT_UI_HOST_PORT:-8025}:8025" volumes: - mailpit_data:/data healthcheck: test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:8025/"] interval: 5s timeout: 3s retries: 12 start_period: 5s networks: [backend, edge] # FastAPI HTTP process. Never parses statement files: those belong to the worker. migrate: build: *backend-build restart: "no" command: ["alembic", "upgrade", "head"] environment: <<: *api-env DATABASE_URL: postgresql+psycopg://${POSTGRES_USER:?POSTGRES_USER is required}:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}@postgres:5432/${POSTGRES_DB:-finance} depends_on: postgres: condition: service_healthy networks: [backend] api: build: *backend-build restart: unless-stopped environment: *api-env ports: - "127.0.0.1:${API_HOST_PORT:-8000}:8000" depends_on: postgres: condition: service_healthy redis: condition: service_healthy minio: condition: service_healthy mailpit: condition: service_started migrate: condition: service_completed_successfully healthcheck: test: ["CMD", "python", "-c", "import urllib.request as u; u.urlopen('http://127.0.0.1:8000/healthz', timeout=3)"] interval: 10s timeout: 5s retries: 12 start_period: 20s networks: [backend, edge] # Celery worker for statement parsing, exports and deletion purge. # Production platform must enforce no outbound network access for this service. worker: build: *backend-build restart: unless-stopped command: ["celery", "-A", "app.worker.celery", "worker", "--loglevel=INFO", "--concurrency=2"] environment: *api-env depends_on: postgres: condition: service_healthy redis: condition: service_healthy minio: condition: service_healthy migrate: condition: service_completed_successfully networks: [backend] beat: build: *backend-build restart: unless-stopped command: ["celery", "-A", "app.worker.celery", "beat", "--loglevel=INFO"] environment: *api-env depends_on: postgres: condition: service_healthy redis: condition: service_healthy migrate: condition: service_completed_successfully networks: [backend] # SPA static files + same-origin reverse proxy for /api/v1 (infra/nginx/nginx.conf). frontend: build: context: ../frontend dockerfile: ../infra/docker/Dockerfile.frontend network: host args: - HTTP_PROXY - HTTPS_PROXY - NO_PROXY additional_contexts: infra: ../infra restart: unless-stopped ports: - "127.0.0.1:${FRONTEND_HOST_PORT:-8080}:80" depends_on: api: condition: service_started networks: [edge] volumes: postgres_data: redis_data: minio_data: mailpit_data: networks: backend: internal: true edge: