services: # ── Frontend (nginx — serves React SPA + proxies API) ─────────────────────── # Default mode: exposes HTTPS on APP_PORT and HTTP on APP_HTTP_PORT. # HTTP is intended for use behind a reverse proxy that terminates TLS. # Production mode (--profile https): Caddy handles TLS instead (see below) frontend: build: context: . dockerfile: frontend/Dockerfile args: VITE_BUILD_SHA: ${GIT_SHA:-dev} VITE_EMAIL_DIV_RENDER: ${VITE_EMAIL_DIV_RENDER:-false} container_name: mailflow-frontend restart: unless-stopped ports: - "${APP_PORT:-443}:443" - "${APP_HTTP_PORT:-80}:80" volumes: - ./certs:/etc/nginx/ssl depends_on: backend: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -qO/dev/null http://127.0.0.1:80/ || exit 1"] interval: 15s timeout: 5s retries: 3 start_period: 20s networks: - mailflow # ── Backend API ────────────────────────────────────────────────────────────── backend: build: context: . dockerfile: backend/Dockerfile args: BUILD_SHA: ${GIT_SHA:-dev} container_name: mailflow-backend restart: unless-stopped environment: NODE_ENV: production PORT: 3000 SESSION_SECRET: ${SESSION_SECRET} APP_URL: ${APP_URL:-} FRONTEND_URL: ${APP_URL:-} DB_HOST: postgres DB_NAME: mailflow DB_USER: mailflow DB_PASSWORD: ${DB_PASSWORD} REDIS_URL: redis://redis:6379 GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-} GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-} ENCRYPTION_KEY: ${ENCRYPTION_KEY:-} VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY:-} VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY:-} VAPID_SUBJECT: ${VAPID_SUBJECT:-} depends_on: postgres: condition: service_healthy redis: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:3000/api/health || exit 1"] interval: 15s timeout: 5s retries: 3 start_period: 30s networks: - mailflow # ── PostgreSQL ─────────────────────────────────────────────────────────────── postgres: image: postgres:16-alpine container_name: mailflow-postgres restart: unless-stopped environment: POSTGRES_DB: mailflow POSTGRES_USER: mailflow POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U mailflow -d mailflow"] interval: 10s timeout: 5s retries: 5 start_period: 20s networks: - mailflow # ── Redis (sessions + caching) ─────────────────────────────────────────────── redis: image: redis:7-alpine container_name: mailflow-redis restart: unless-stopped command: redis-server --save 60 1 --loglevel warning volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 3s retries: 5 start_period: 10s networks: - mailflow # ── Caddy (automatic HTTPS via Let's Encrypt — production only) ────────────── # Use docker-compose.https.yml to avoid the port conflict with the frontend: # docker compose -f docker-compose.yml -f docker-compose.https.yml --profile https up -d --build # Requires DOMAIN, ACME_EMAIL, and APP_URL to be set in .env, and ports 80/443 to be open. caddy: profiles: ["https"] image: caddy:2-alpine container_name: mailflow-caddy restart: unless-stopped ports: - "80:80" - "443:443" - "443:443/udp" environment: DOMAIN: ${DOMAIN:-localhost} ACME_EMAIL: ${ACME_EMAIL:-} volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config depends_on: - frontend networks: - mailflow volumes: postgres_data: redis_data: caddy_data: caddy_config: networks: mailflow: driver: bridge