services: # Runs database migrations once before the API and worker start. migrate: image: git.devils.zone/devil/youtube-studio-flow-backend:${IMAGE_TAG:-latest} pull_policy: always command: npx prisma migrate deploy restart: "no" environment: DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} depends_on: postgres: condition: service_healthy networks: - app-network api: image: git.devils.zone/devil/youtube-studio-flow-backend:${IMAGE_TAG:-latest} pull_policy: always restart: unless-stopped environment: NODE_ENV: production PORT: 3001 DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379 JWT_SECRET: ${JWT_SECRET} JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET} TOKEN_ENCRYPTION_KEY: ${TOKEN_ENCRYPTION_KEY} GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID} GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET} GOOGLE_CALLBACK_URL: ${GOOGLE_CALLBACK_URL} FRONTEND_URL: ${FRONTEND_URL} labels: - "traefik.enable=true" - "traefik.http.routers.studioflow-api.rule=Host(`${DOMAIN}`) && PathPrefix(`/api`)" - "traefik.http.routers.studioflow-api.entrypoints=websecure" - "traefik.http.routers.studioflow-api.tls.certresolver=letsencrypt" - "traefik.http.routers.studioflow-api.priority=10" - "traefik.http.services.studioflow-api.loadbalancer.server.port=3001" depends_on: postgres: condition: service_healthy redis: condition: service_healthy migrate: condition: service_completed_successfully healthcheck: test: ["CMD-SHELL", "wget -qO- http://localhost:3001/api/v1/health || exit 1"] interval: 15s timeout: 5s retries: 3 start_period: 30s networks: - app-network - proxy worker: image: git.devils.zone/devil/youtube-studio-flow-backend:${IMAGE_TAG:-latest} pull_policy: always restart: unless-stopped command: node dist/worker.js environment: NODE_ENV: production DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379 JWT_SECRET: ${JWT_SECRET} JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET} TOKEN_ENCRYPTION_KEY: ${TOKEN_ENCRYPTION_KEY} GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID} GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET} CONFLICT_DETECTION_ENABLED: ${CONFLICT_DETECTION_ENABLED:-false} CONFLICT_DETECTION_CRON: ${CONFLICT_DETECTION_CRON:-0 3 * * *} depends_on: postgres: condition: service_healthy redis: condition: service_healthy migrate: condition: service_completed_successfully networks: - app-network frontend: # NEXT_PUBLIC_API_URL is baked in at build time (scripts/build-and-push.sh uses the # Dockerfile default, /api/v1) — the relative path works because Traefik serves both # frontend and API on the same domain. image: git.devils.zone/devil/youtube-studio-flow-frontend:${IMAGE_TAG:-latest} pull_policy: always restart: unless-stopped labels: - "traefik.enable=true" - "traefik.http.routers.studioflow-frontend.rule=Host(`${DOMAIN}`)" - "traefik.http.routers.studioflow-frontend.entrypoints=websecure" - "traefik.http.routers.studioflow-frontend.tls.certresolver=letsencrypt" - "traefik.http.routers.studioflow-frontend.priority=1" - "traefik.http.services.studioflow-frontend.loadbalancer.server.port=3000" depends_on: api: condition: service_healthy networks: - app-network - proxy postgres: image: postgres:16-alpine restart: unless-stopped # Bound to localhost only — not exposed to the public internet. ports: - "127.0.0.1:5432:5432" 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 networks: - app-network redis: image: redis:7-alpine restart: unless-stopped command: > redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes --maxmemory-policy noeviction volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] interval: 5s timeout: 3s retries: 10 networks: - app-network volumes: postgres_data: redis_data: networks: app-network: driver: bridge proxy: external: true