# Production stack: [your reverse proxy] -> web (nginx: SPA + /api proxy) -> backend + mysql. # # cp .env.example .env.production # fill the PRODUCTION section first # docker compose --env-file .env.production -f docker-compose.prod.yml up -d --build # # --env-file is REQUIRED: the ${AR_DOMAIN} / ${MYSQL_ROOT_PASSWORD} references below are # resolved from it (env_file: alone only feeds the containers, not this YAML). # # SHARED SERVER (default): the app's only host port is 127.0.0.1:81 (the web UI). Point # the server's reverse proxy for ar.utopiabrands.com at http://127.0.0.1:81 with # client_max_body_size 2g; proxy_read_timeout 600s; proxy_request_buffering off; # All other ports (backend 8000, mysql 3306) are container-internal and can never # conflict with other apps on the box. # # DEDICATED SERVER: nothing else on 80/443? Start the bundled auto-HTTPS front instead: # docker compose --env-file .env.production -f docker-compose.prod.yml --profile caddy up -d --build # # Sized for one 8 GB server (300-500 MB Excel parsing needs the RAM). Backend runs ONE # worker by design — jobs and their progress live in-process. See deploy/DEPLOY.md. services: mysql: image: mysql:8.4 restart: unless-stopped env_file: .env.production # uses MYSQL_PASSWORD / MYSQL_DATABASE / MYSQL_USER environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?set MYSQL_ROOT_PASSWORD in .env.production} command: - --innodb-buffer-pool-size=1G - --max-allowed-packet=256M volumes: - mysql_data:/var/lib/mysql healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p${MYSQL_ROOT_PASSWORD}"] interval: 10s timeout: 5s retries: 12 # Not exposed to the host network — only the backend reaches it. backend: build: ./backend restart: unless-stopped env_file: .env.production environment: AR_DATA_DIR: /data MYSQL_HOST: mysql AR_DB_BACKEND: mysql volumes: - ar_data:/data depends_on: mysql: condition: service_healthy healthcheck: test: ["CMD", "python", "-c", "import urllib.request;urllib.request.urlopen('http://localhost:8000/api/health', timeout=5)"] interval: 30s timeout: 10s retries: 3 web: build: context: ./frontend target: prod restart: unless-stopped depends_on: - backend ports: # Loopback-only: reachable by the server's own reverse proxy, never the internet. # Host port 81 avoids clashing with anything else on a shared box. - "127.0.0.1:81:80" # OPTIONAL auto-HTTPS front for a DEDICATED server (--profile caddy). Not started by # default: on a shared box another proxy usually owns 80/443 already. caddy: image: caddy:2-alpine restart: unless-stopped profiles: ["caddy"] ports: - "80:80" - "443:443" environment: AR_DOMAIN: ${AR_DOMAIN:?set AR_DOMAIN in .env.production} command: caddy reverse-proxy --from "https://${AR_DOMAIN}" --to web:80 volumes: - caddy_data:/data - caddy_config:/config volumes: mysql_data: ar_data: caddy_data: caddy_config: