services: # --- PostgreSQL (launch-ready cutover target) --- postgres: image: postgres:16-alpine environment: - POSTGRES_DB=${POSTGRES_DB:-dociva} - POSTGRES_USER=${POSTGRES_USER:-dociva} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-change-me} volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-dociva} -d ${POSTGRES_DB:-dociva}"] interval: 10s timeout: 5s retries: 5 restart: always # --- Redis --- redis: image: redis:7-alpine volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 3s retries: 5 restart: always # --- Flask Backend --- backend: build: context: ./backend dockerfile: Dockerfile env_file: - .env environment: - FLASK_ENV=production - REDIS_URL=redis://redis:6379/0 - CELERY_BROKER_URL=redis://redis:6379/0 - CELERY_RESULT_BACKEND=redis://redis:6379/1 - DATABASE_URL=${DATABASE_URL:-} volumes: - upload_data:/tmp/uploads - output_data:/tmp/outputs - db_data:/app/data depends_on: postgres: condition: service_healthy redis: condition: service_healthy restart: always # --- Celery Worker --- celery_worker: build: context: ./backend dockerfile: Dockerfile command: > celery -A celery_worker.celery worker --loglevel=warning --concurrency=4 -Q default,convert,compress,image,video,pdf_tools,flowchart,ai_heavy env_file: - .env environment: - FLASK_ENV=production - REDIS_URL=redis://redis:6379/0 - CELERY_BROKER_URL=redis://redis:6379/0 - CELERY_RESULT_BACKEND=redis://redis:6379/1 - DATABASE_URL=${DATABASE_URL:-} volumes: - upload_data:/tmp/uploads - output_data:/tmp/outputs - db_data:/app/data depends_on: postgres: condition: service_healthy redis: condition: service_healthy healthcheck: test: ["CMD", "true"] interval: 30s timeout: 5s retries: 1 restart: always # --- Celery Beat (Scheduled Tasks) --- celery_beat: build: context: ./backend dockerfile: Dockerfile command: > celery -A celery_worker.celery beat --loglevel=warning env_file: - .env environment: - FLASK_ENV=production - REDIS_URL=redis://redis:6379/0 - CELERY_BROKER_URL=redis://redis:6379/0 - CELERY_RESULT_BACKEND=redis://redis:6379/1 - DATABASE_URL=${DATABASE_URL:-} volumes: - db_data:/app/data depends_on: postgres: condition: service_healthy redis: condition: service_healthy healthcheck: test: ["CMD", "true"] interval: 30s timeout: 5s retries: 1 restart: always # --- Nginx (serves built frontend + reverse proxy) --- nginx: image: nginx:alpine ports: - "80:80" - "443:443" volumes: - ./nginx/nginx.prod.conf:/etc/nginx/conf.d/default.conf:ro - frontend_build:/usr/share/nginx/html:ro - ./certbot/conf:/etc/letsencrypt:ro - ./certbot/www:/var/www/certbot:ro depends_on: - backend - frontend_build_step restart: always # --- Frontend Build (one-shot) --- frontend_build_step: build: context: ./frontend dockerfile: Dockerfile target: build args: VITE_GA_MEASUREMENT_ID: ${VITE_GA_MEASUREMENT_ID:-} VITE_PLAUSIBLE_DOMAIN: ${VITE_PLAUSIBLE_DOMAIN:-} VITE_PLAUSIBLE_SRC: ${VITE_PLAUSIBLE_SRC:-https://plausible.io/js/script.js} VITE_GOOGLE_SITE_VERIFICATION: ${VITE_GOOGLE_SITE_VERIFICATION:-} VITE_ADSENSE_CLIENT_ID: ${VITE_ADSENSE_CLIENT_ID:-} VITE_ADSENSE_SLOT_HOME_TOP: ${VITE_ADSENSE_SLOT_HOME_TOP:-} VITE_ADSENSE_SLOT_HOME_BOTTOM: ${VITE_ADSENSE_SLOT_HOME_BOTTOM:-} VITE_ADSENSE_SLOT_TOP_BANNER: ${VITE_ADSENSE_SLOT_TOP_BANNER:-} VITE_ADSENSE_SLOT_BOTTOM_BANNER: ${VITE_ADSENSE_SLOT_BOTTOM_BANNER:-} VITE_FEATURE_EDITOR: ${VITE_FEATURE_EDITOR:-true} VITE_FEATURE_OCR: ${VITE_FEATURE_OCR:-true} VITE_FEATURE_REMOVEBG: ${VITE_FEATURE_REMOVEBG:-true} VITE_SITE_DOMAIN: ${VITE_SITE_DOMAIN:-} VITE_SENTRY_DSN: ${VITE_SENTRY_DSN:-} VITE_CLARITY_PROJECT_ID: ${VITE_CLARITY_PROJECT_ID:-} INDEXNOW_KEY: ${INDEXNOW_KEY:-} INDEXNOW_ENDPOINT: ${INDEXNOW_ENDPOINT:-https://www.bing.com/indexnow} INDEXNOW_STRICT: ${INDEXNOW_STRICT:-false} environment: - VITE_GA_MEASUREMENT_ID=${VITE_GA_MEASUREMENT_ID:-} - VITE_PLAUSIBLE_DOMAIN=${VITE_PLAUSIBLE_DOMAIN:-} - VITE_PLAUSIBLE_SRC=${VITE_PLAUSIBLE_SRC:-https://plausible.io/js/script.js} - VITE_GOOGLE_SITE_VERIFICATION=${VITE_GOOGLE_SITE_VERIFICATION:-} - VITE_ADSENSE_CLIENT_ID=${VITE_ADSENSE_CLIENT_ID:-} - VITE_ADSENSE_SLOT_HOME_TOP=${VITE_ADSENSE_SLOT_HOME_TOP:-} - VITE_ADSENSE_SLOT_HOME_BOTTOM=${VITE_ADSENSE_SLOT_HOME_BOTTOM:-} - VITE_ADSENSE_SLOT_TOP_BANNER=${VITE_ADSENSE_SLOT_TOP_BANNER:-} - VITE_ADSENSE_SLOT_BOTTOM_BANNER=${VITE_ADSENSE_SLOT_BOTTOM_BANNER:-} - VITE_FEATURE_EDITOR=${VITE_FEATURE_EDITOR:-true} - VITE_FEATURE_OCR=${VITE_FEATURE_OCR:-true} - VITE_FEATURE_REMOVEBG=${VITE_FEATURE_REMOVEBG:-true} - VITE_SITE_DOMAIN=${VITE_SITE_DOMAIN:-} - VITE_SENTRY_DSN=${VITE_SENTRY_DSN:-} - VITE_CLARITY_PROJECT_ID=${VITE_CLARITY_PROJECT_ID:-} - INDEXNOW_KEY=${INDEXNOW_KEY:-} - INDEXNOW_ENDPOINT=${INDEXNOW_ENDPOINT:-https://www.bing.com/indexnow} - INDEXNOW_STRICT=${INDEXNOW_STRICT:-false} - INDEXNOW_STATE_DIR=/app/.indexnow - INDEXNOW_FULL_SUBMIT=${INDEXNOW_FULL_SUBMIT:-false} volumes: - frontend_build:/app/dist - indexnow_state:/app/.indexnow volumes: postgres_data: redis_data: upload_data: output_data: db_data: frontend_build: indexnow_state: