services:
  backend:
    image: ${ECR_REGISTRY}/helix-server:${IMAGE_TAG:-latest}
    restart: unless-stopped
    env_file: /opt/helix/env/.env.production
    environment:
      CORS_ORIGINS: '["https://helix.pingcap.net"]'
    volumes:
      - /opt/helix/ca-certificates:/etc/helix/ca-certificates:ro
    # Bind to loopback ONLY (was 0.0.0.0:8000). nginx reaches the
    # backend over the compose ``helix`` network via service DNS
    # (proxy_pass http://backend:8000), so the host port was never
    # needed for the request path — it only widened the attack surface.
    # Security review 2026-06-11: a 0.0.0.0 publish let an external
    # client reach :8000 directly through docker-proxy, where the
    # backend saw the bridge gateway (172.x.0.1) as the peer — which,
    # being inside the trusted-proxy CIDR, was treated as a trusted
    # reverse proxy and could spoof X-Forwarded-For (defeating every
    # per-IP rate limiter + poisoning audit ip_address). Loopback bind
    # closes that path; the explicit network below pins the trusted
    # range so the CIDR can be tight instead of the whole 172.16/12.
    ports:
      - "127.0.0.1:8000:8000"
    networks:
      - helix
    # The python:3.12-slim base image doesn't ship curl, so the
    # previous ``curl -f`` healthcheck always errored and the
    # container showed ``unhealthy`` even though the API was serving
    # 200s. Use stdlib urllib (zero extra deps) and exit 0/1 based on
    # the actual HTTP status code.
    healthcheck:
      test:
        - CMD
        - /app/.venv/bin/python
        - -c
        - "import sys, urllib.request; sys.exit(0 if urllib.request.urlopen('http://localhost:8000/api/v1/ready', timeout=5).status == 200 else 1)"
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 15s

  worker:
    image: ${ECR_REGISTRY}/helix-worker:${IMAGE_TAG:-latest}
    restart: unless-stopped
    env_file: /opt/helix/env/.env.production
    command: ["/app/.venv/bin/python", "-m", "app.worker"]
    volumes:
      - /opt/helix/ca-certificates:/etc/helix/ca-certificates:ro
    # Give the graceful-shutdown drain room: on SIGTERM the worker pauses
    # the scheduler, drains in-flight jobs for SHUTDOWN_DRAIN_GRACE_SECONDS
    # (8s), then cancels stragglers — and a cancelled identity-sync sweep
    # must still finalise its run row (status='cancelled') via a DB write
    # inside the CancelledError handler. Docker's 10s default SIGTERM→
    # SIGKILL window leaves too little margin for drain + cancel-finalise;
    # 30s ensures the run row is always cleanly finalised so the next tick
    # re-runs it instead of leaving a phantom 'running' row for cleanup.
    stop_grace_period: 30s
    networks:
      - helix
    healthcheck:
      # Real readiness/liveness signal written by app.worker only AFTER the
      # enabled-handler boot guard passes, then refreshed by its event loop.
      # The previous constant-zero probe marked a crash-looping worker healthy
      # and let deploys pass while every scheduled job was stopped.
      test:
        - CMD
        - /app/.venv/bin/python
        - -c
        - "import pathlib,sys,time; p=pathlib.Path('/tmp/helix-worker-heartbeat'); sys.exit(0 if p.exists() and time.time()-p.stat().st_mtime < 120 else 1)"
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 30s

  frontend:
    image: ${ECR_REGISTRY}/helix-web:${IMAGE_TAG:-latest}
    restart: unless-stopped
    ports:
      - "80:80"
    networks:
      - helix
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:80/"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 5s

# Pinned bridge subnet so the backend's trusted-proxy CIDR can be the
# exact compose network range (172.28.0.0/16) rather than the whole
# RFC1918 172.16/12 block. nginx's container IP lands in this subnet;
# the backend trusts X-Forwarded-For only from it. Keep this subnet in
# lock-step with the TRUSTED_PROXY_IPS default in
# deploy/scripts/render-env.sh.
networks:
  helix:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16
