services: db: image: postgres:16-alpine restart: unless-stopped env_file: .env environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 5s timeout: 5s retries: 10 deploy: resources: limits: memory: 512M app: build: context: ./backend dockerfile: Dockerfile restart: unless-stopped env_file: .env environment: # Activates the production secret guard in config.rs — refuses to boot with # placeholder JWT_SECRET / ADMIN_PASSWORD_HASH. APP_ENV: production depends_on: db: condition: service_healthy volumes: - media_data:/media # Export archives live OUTSIDE /media so the public media ServeDir can't # serve them — downloads go only through the ticket-gated handler. - exports_data:/exports expose: - "3000" healthcheck: test: ["CMD-SHELL", "wget -q -O- http://localhost:3000/health || exit 1"] interval: 10s timeout: 5s retries: 5 start_period: 20s deploy: resources: limits: # Bounds a runaway ffmpeg transcode (large uploads, 2 workers) so it can't # OOM the single box and take down Postgres. memory: 1G frontend: build: context: ./frontend dockerfile: Dockerfile restart: unless-stopped env_file: .env environment: # adapter-node behind Caddy TLS needs the public origin for CSRF checks on # POST form actions — without it they fail only in production. ORIGIN: "https://${DOMAIN}" depends_on: - app expose: - "3001" healthcheck: test: ["CMD-SHELL", "wget -q -O- http://localhost:3001/ >/dev/null 2>&1 || exit 1"] interval: 10s timeout: 5s retries: 5 start_period: 15s deploy: resources: limits: memory: 256M caddy: image: caddy:2-alpine restart: unless-stopped ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data depends_on: app: condition: service_healthy frontend: condition: service_healthy deploy: resources: limits: memory: 256M volumes: postgres_data: media_data: exports_data: caddy_data: