# Production stack: nginx (TLS via Cloudflare Origin Certificate, serves built SPA + # reverse-proxies) -> gunicorn -> Postgres. # # 1) Point DNS (e.g. timeline.example.com) at the server's elastic IP (Cloudflare, # proxied / orange-cloud), and set the SSL/TLS mode to "Full (strict)". # 2) Set prod secrets + DOMAIN in .env. # 3) Place the Cloudflare Origin Certificate at nginx/certs/origin.pem + origin.key. # 4) docker compose -f docker-compose.prod.yml up -d --build # # migrate + collectstatic run automatically on backend start. services: db: image: postgres:16-alpine restart: always environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 5 backend: build: context: ./backend restart: always environment: RUN_COLLECTSTATIC: "1" POSTGRES_HOST: db WEB_CONCURRENCY: ${WEB_CONCURRENCY:-3} # gunicorn workers; tune to instance size env_file: - .env volumes: - static_volume:/app/staticfiles - media_volume:/app/media expose: - "8000" depends_on: db: condition: service_healthy nginx: build: context: ./frontend target: prod restart: always environment: DOMAIN: ${DOMAIN} NGINX_ENVSUBST_FILTER: DOMAIN # only substitute ${DOMAIN}, leave nginx $vars alone volumes: - static_volume:/app/staticfiles:ro - media_volume:/app/media:ro - ./nginx/nginx.prod.conf:/etc/nginx/templates/default.conf.template:ro - ./nginx/certs:/etc/nginx/certs:ro # Cloudflare Origin Certificate (origin.pem + origin.key) ports: - "80:80" - "443:443" depends_on: - backend volumes: pgdata: static_volume: media_volume: