version: "3.9" # --------------------------------------------------------------------------- # Bulk Gmail Mailer — production-style compose stack. # # Services: # postgres - primary datastore (18-table schema, migrated via alembic) # redis - celery broker/result backend + rate-limit/DLQ/pubsub storage # migrate - one-shot: runs `alembic upgrade head`, then exits # api - FastAPI app served by gunicorn+uvicorn workers (port 8000) # worker - celery worker consuming the `send` queue (real Gmail sending) # # Usage: # cp .env.example .env # fill in real secrets first # docker compose up -d --build # docker compose logs -f api worker # # The API is reachable at http://localhost:8000 (see README for HTTPS/reverse # proxy notes — this compose file does not terminate TLS itself). # --------------------------------------------------------------------------- services: postgres: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_USER: mailer POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mailer_prod_pw} POSTGRES_DB: bulkmailer volumes: - pg_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U mailer -d bulkmailer"] interval: 5s timeout: 5s retries: 10 ports: - "127.0.0.1:5432:5432" # remove/comment in real production; internal-only is safer redis: image: redis:7-alpine restart: unless-stopped command: ["redis-server", "--appendonly", "yes"] volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 5s retries: 10 ports: - "127.0.0.1:6379:6379" # remove/comment in real production; internal-only is safer migrate: build: . env_file: .env depends_on: postgres: condition: service_healthy entrypoint: ["alembic", "upgrade", "head"] restart: "no" api: build: . restart: unless-stopped env_file: .env depends_on: postgres: condition: service_healthy redis: condition: service_healthy migrate: condition: service_completed_successfully ports: - "8000:8000" volumes: - uploads_data:/app/uploads environment: DATABASE_URL: ${DATABASE_URL:-postgresql+psycopg://mailer:mailer_prod_pw@postgres:5432/bulkmailer} REDIS_URL: ${REDIS_URL:-redis://redis:6379/0} # NOTE: set ENCRYPTION_KEY explicitly in .env for production. If left # blank, each container auto-generates its OWN key file at # /app/.encryption_key on first boot — since api/worker are separate # containers without a shared volume for it, that would make tokens # encrypted by one unreadable by the other. Always set ENCRYPTION_KEY. worker: build: . restart: unless-stopped env_file: .env depends_on: postgres: condition: service_healthy redis: condition: service_healthy migrate: condition: service_completed_successfully volumes: - uploads_data:/app/uploads environment: DATABASE_URL: ${DATABASE_URL:-postgresql+psycopg://mailer:mailer_prod_pw@postgres:5432/bulkmailer} REDIS_URL: ${REDIS_URL:-redis://redis:6379/0} # worker_concurrency is set in app/worker/celery_app.py; scale replicas # (docker compose up -d --scale worker=3) for higher throughput instead # of raising in-process concurrency, to keep per-account rate limiting # (enforced in Redis) predictable across workers. entrypoint: ["celery", "-A", "app.worker.celery_app", "worker", "--loglevel=INFO", "-Q", "send,control"] volumes: pg_data: redis_data: uploads_data: