# Stack gọn cho demo / Fly.io (<50 users): Postgres + API + Vue + Nginx # ML (speaking/shadowing) chạy inline trong API — không cần Redis, MinIO, Celery, PgBouncer. services: db: image: postgres:17-alpine environment: POSTGRES_USER: linguaielts POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: linguaielts volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U linguaielts"] interval: 10s timeout: 5s retries: 5 api: build: ./backend user: "0" command: > sh -c "mkdir -p /app/uploads/avatars && chown -R appuser:appuser /app/uploads && for i in 1 2 3 4 5 6 7 8 9 10; do su appuser -s /bin/sh -c 'alembic upgrade head' && break; echo \"Waiting for database ($$i/10)…\"; sleep 4; done && exec su appuser -s /bin/sh -c 'exec gunicorn app.main:app -w 2 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 --timeout 300 --access-logfile -'" environment: DATABASE_URL: postgresql+asyncpg://linguaielts:${DB_PASSWORD}@db:5432/linguaielts PGBOUNCER_ENABLED: "false" REDIS_URL: "" REDIS_REQUIRED: "false" CELERY_ENABLED: "false" SECRET_KEY: ${SECRET_KEY} OPENROUTER_API_KEY: ${OPENROUTER_API_KEY} FRONTEND_ORIGIN: ${FRONTEND_ORIGIN} SENTRY_DSN: ${SENTRY_DSN:-} ENVIRONMENT: production AUTO_CREATE_TABLES: "false" STORAGE_BACKEND: ${STORAGE_BACKEND:-local} CLOUDINARY_CLOUD_NAME: ${CLOUDINARY_CLOUD_NAME:-} CLOUDINARY_API_KEY: ${CLOUDINARY_API_KEY:-} CLOUDINARY_API_SECRET: ${CLOUDINARY_API_SECRET:-} AVATAR_UPLOAD_DIR: uploads/avatars METRICS_ENABLED: "true" METRICS_TOKEN: ${METRICS_TOKEN} ML_PRELOAD_ON_STARTUP: "true" AUTH_HTTPONLY_REFRESH: "true" AUTH_COOKIE_SECURE: "false" REQUIRE_EMAIL_VERIFICATION: "false" SMTP_HOST: ${SMTP_HOST:-} SMTP_PORT: ${SMTP_PORT:-587} SMTP_USER: ${SMTP_USER:-} SMTP_PASSWORD: ${SMTP_PASSWORD:-} SMTP_FROM: ${SMTP_FROM:-} SMTP_USE_TLS: ${SMTP_USE_TLS:-true} PASSWORD_RESET_EXPIRE_HOURS: ${PASSWORD_RESET_EXPIRE_HOURS:-24} GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-} GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-} GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI:-http://localhost/auth/google/callback} depends_on: db: condition: service_healthy volumes: - ./backend/data:/app/data - uploads:/app/uploads expose: - "8000" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 30s timeout: 10s retries: 3 start_period: 120s restart: unless-stopped frontend: build: context: ./fronted args: VITE_GOOGLE_CLIENT_ID: ${VITE_GOOGLE_CLIENT_ID:-} VITE_GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI:-http://localhost/auth/google/callback} VITE_AUDIO_CDN_BASE: /api/audio VITE_AUDIO_CDN_EXT: .mp3 VITE_CLOUDINARY_CLOUD_NAME: ${CLOUDINARY_CLOUD_NAME:-dq955rfxi} environment: BACKEND_INTERNAL_URL: http://api:8000 expose: - "80" restart: unless-stopped gateway: image: nginx:alpine ports: - "80:80" volumes: - ./nginx.docker.conf:/etc/nginx/conf.d/default.conf:ro depends_on: api: condition: service_healthy frontend: condition: service_started healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1/"] interval: 30s timeout: 5s retries: 3 restart: unless-stopped volumes: pgdata: uploads: