# Base topology (design D14). This file is never used alone — the Makefile always pairs it
# with docker-compose.dev.yml or docker-compose.prod.yml so no environment is implicit.
#
# Five services, one of them stateful. Postgres is both the database and the job queue
# (design D3), so there is no Redis and only one thing to back up.

services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-rsvp}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-rsvp}
      POSTGRES_DB: ${POSTGRES_DB:-rsvp}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-rsvp} -d ${POSTGRES_DB:-rsvp}"]
      interval: 5s
      timeout: 5s
      retries: 10

  api:
    build:
      context: ./api
      # `prod` is the last stage in the Dockerfile and therefore the default; naming it
      # here means a reader does not have to know that to know what gets built. The dev
      # overlay switches this to `dev`, which is the only stage carrying pytest and mypy.
      target: prod
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-rsvp}:${POSTGRES_PASSWORD:-rsvp}@postgres:5432/${POSTGRES_DB:-rsvp}
    volumes:
      # Card artwork (design D4). A bind mount rather than a named volume so `make backup`
      # can archive it and a human can look at what was uploaded. This is the second piece
      # of state in the system — Postgres is no longer the only thing to back up.
      - ./media:/srv/media
    expose:
      - "8000"
    # /api/health returns 503 only when the database is unreachable — a stale worker or an
    # unconfigured provider reports `degraded` with a 200 on purpose, so this marks the
    # container unhealthy exactly when it cannot serve, and not merely when something is
    # wrong. `start_period` covers migrations running against a cold database.
    healthcheck:
      test:
        [
          "CMD",
          "python",
          "-c",
          "import urllib.request;urllib.request.urlopen('http://127.0.0.1:8000/api/health',timeout=5)",
        ]
      interval: 15s
      timeout: 10s
      retries: 5
      start_period: 40s

  worker:
    build:
      context: ./api
      target: prod
    command: ["python", "-m", "app.worker.main"]
    # No healthcheck: the worker serves no port, and its liveness is already reported —
    # `/api/health` reads the heartbeat table and says `stale` when a job stops ticking.
    # A container-level check here would have to poll the database to learn the same
    # thing, and a process that is running but wedged would still pass it.
    depends_on:
      postgres:
        condition: service_healthy
    environment:
      DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-rsvp}:${POSTGRES_PASSWORD:-rsvp}@postgres:5432/${POSTGRES_DB:-rsvp}

  web:
    build:
      context: ./web
    depends_on:
      # Not merely "started": every page in the admin area and the whole invitation route
      # is server-rendered against the API, so a web container that boots first serves a
      # wall of fetch errors for as long as the API takes to come up.
      api:
        condition: service_healthy
    expose:
      - "3000"
    healthcheck:
      test:
        [
          "CMD",
          "node",
          "-e",
          "require('http').get('http://127.0.0.1:3000/',r=>process.exit(r.statusCode<500?0:1)).on('error',()=>process.exit(1))",
        ]
      interval: 15s
      timeout: 10s
      retries: 5
      # Generous because in development this stage is `next dev`, which compiles the first
      # route on demand rather than at boot.
      start_period: 90s

  caddy:
    image: caddy:2-alpine
    depends_on:
      - web
      - api
    volumes:
      - caddy_data:/data
      - caddy_config:/config
      # Read-only: Caddy serves card artwork straight off disk and must never be able to
      # write it. Uploads are the API's job.
      - ./media:/srv/media:ro

volumes:
  postgres_data:
  caddy_data:
  caddy_config:
