services: backend: build: context: ./backend dockerfile: Dockerfile target: production restart: unless-stopped ports: - "8090:8000" environment: DATABASE_URL: "postgresql+asyncpg://finance_app:${DB_PASSWORD}@postgres:5432/financedb" REDIS_URL: "redis://:${REDIS_PASSWORD}@redis:6379/0" ENCRYPTION_KEY: "${ENCRYPTION_KEY}" BACKUP_PASSPHRASE: "${BACKUP_PASSPHRASE}" ENVIRONMENT: "${ENVIRONMENT:-production}" ALLOW_REGISTRATION: "${ALLOW_REGISTRATION:-false}" BASE_CURRENCY: "${BASE_CURRENCY:-GBP}" volumes: - ./secrets:/run/secrets:ro - ./data/backups:/app/backups - ./data/uploads:/app/uploads depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - frontend_net - backend_net security_opt: - no-new-privileges:true healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 30s timeout: 10s retries: 3 start_period: 60s frontend: build: context: ./frontend dockerfile: Dockerfile target: production restart: unless-stopped ports: - "4000:3000" networks: - frontend_net security_opt: - no-new-privileges:true read_only: true tmpfs: - /tmp - /var/cache/nginx - /var/run postgres: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_DB: financedb POSTGRES_USER: finance_app POSTGRES_PASSWORD: "${DB_PASSWORD}" volumes: - ./data/postgres:/var/lib/postgresql/data - ./postgres/init:/docker-entrypoint-initdb.d:ro networks: - backend_net security_opt: - no-new-privileges:true healthcheck: test: ["CMD-SHELL", "pg_isready -U finance_app -d financedb"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine restart: unless-stopped command: > redis-server /usr/local/etc/redis/redis.conf --requirepass "${REDIS_PASSWORD}" volumes: - redis_data:/data - ./redis/redis.conf:/usr/local/etc/redis/redis.conf:ro networks: - backend_net security_opt: - no-new-privileges:true healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] interval: 10s timeout: 5s retries: 3 volumes: redis_data: networks: frontend_net: driver: bridge backend_net: driver: bridge internal: true