services: db: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_DB: jobsquatch POSTGRES_USER: postgres POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set} volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 10 networks: - jobsquatch-internal redis: image: redis:7-alpine restart: unless-stopped volumes: - redis_data:/data command: redis-server --appendonly yes networks: - jobsquatch-internal app: image: ghcr.io/teamcastaldi/jobsquatch-backend:${IMAGE_TAG:-latest} restart: unless-stopped depends_on: db: condition: service_healthy redis: condition: service_started environment: APP_ENV: production DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}@db:5432/jobsquatch REDIS_URL: redis://redis:6379/0 SECRET_KEY: ${SECRET_KEY:?SECRET_KEY must be set} ACCESS_TOKEN_EXPIRE_DAYS: ${ACCESS_TOKEN_EXPIRE_DAYS:-7} APP_BASE_URL: ${APP_BASE_URL:?APP_BASE_URL must be set} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:?ANTHROPIC_API_KEY must be set} RESEND_API_KEY: ${RESEND_API_KEY:?RESEND_API_KEY must be set} EMAIL_FROM: ${EMAIL_FROM:?EMAIL_FROM must be set} STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:?STRIPE_SECRET_KEY must be set} STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET:?STRIPE_WEBHOOK_SECRET must be set} STRIPE_STARTER_PRICE_ID: ${STRIPE_STARTER_PRICE_ID:?STRIPE_STARTER_PRICE_ID must be set} STRIPE_PACK_PRICE_ID: ${STRIPE_PACK_PRICE_ID:?STRIPE_PACK_PRICE_ID must be set} STRIPE_BETA_PRICE_ID: ${STRIPE_BETA_PRICE_ID:?STRIPE_BETA_PRICE_ID must be set} CORS_ORIGINS: ${CORS_ORIGINS:-} LOG_LEVEL: ${LOG_LEVEL:-INFO} command: > sh -c "alembic upgrade head && uvicorn src.web.app:app --host 0.0.0.0 --port 8000" healthcheck: test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')\""] interval: 10s timeout: 5s retries: 5 start_period: 30s networks: - jobsquatch-internal frontend: image: ghcr.io/teamcastaldi/jobsquatch-frontend:${IMAGE_TAG:-latest} restart: unless-stopped ports: - "80:80" depends_on: app: condition: service_healthy labels: # When a container is on multiple networks, Traefik picks one arbitrarily. # Pin it to proxy-net so Traefik never tries to route via jobsquatch-internal. traefik.docker.network: proxy-net networks: - jobsquatch-internal - proxy-net labels: - "traefik.enable=true" - "traefik.http.routers.jobsquatch.rule=Host(`jobsquatch-alpha.castaldifamily.com`)" - "traefik.http.routers.jobsquatch.entrypoints=websecure" - "traefik.http.routers.jobsquatch.tls=true" - "traefik.http.routers.jobsquatch.tls.certresolver=cloudflare" - "traefik.http.routers.jobsquatch.middlewares=security-headers@file" - "traefik.http.services.jobsquatch.loadbalancer.server.port=80" volumes: postgres_data: redis_data: networks: # Internal bridge for service-to-service communication (db, redis, app, frontend). # Named explicitly so the full Docker network name is _jobsquatch-internal, # where is the Compose project name (COMPOSE_PROJECT_NAME or directory name). # On heimdall this is jobsquatch-alpha_jobsquatch-internal. jobsquatch-internal: driver: bridge # External network managed by Traefik on heimdall. The frontend must be on this # network so Traefik can route inbound HTTPS traffic to it. proxy-net: external: true