services:
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      # No defaults on purpose: POSTGRES_USER/PASSWORD/DB must be set in your
      # local, git-ignored .env (see .env.example). Compose fails loudly if
      # they're missing instead of falling back to a guessable credential.
      POSTGRES_USER: ${POSTGRES_USER:?set POSTGRES_USER in .env}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
      POSTGRES_DB: ${POSTGRES_DB:?set POSTGRES_DB in .env}
    ports:
      - "${POSTGRES_PORT:-5432}:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
      interval: 5s
      timeout: 5s
      retries: 5

  # Single service serving both the API and the built frontend (see
  # apps/api/Dockerfile) — no separate nginx/web container, no cross-origin
  # CORS_ORIGIN to keep in sync between two ports.
  app:
    build:
      context: .
      dockerfile: apps/api/Dockerfile
    restart: unless-stopped
    environment:
      NODE_ENV: production
      PORT: 3000
      # Uses the "postgres" service name, not localhost/POSTGRES_PORT —
      # container-to-container traffic stays on the compose network and
      # always targets Postgres's internal port (5432).
      DATABASE_URL: "postgresql://${POSTGRES_USER:?set POSTGRES_USER in .env}:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@postgres:5432/${POSTGRES_DB:?set POSTGRES_DB in .env}?schema=public"
      JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET in .env}
      # Unset by default (falls back to NODE_ENV === "production", i.e.
      # Secure cookie required) — set COOKIE_SECURE=false in .env only if
      # this deployment is reachable over plain HTTP (no TLS in front of
      # it yet), otherwise the session cookie never comes back and every
      # authenticated request 401s despite login succeeding. See its doc
      # comment in apps/api/src/config/env.ts.
      COOKIE_SECURE: ${COOKIE_SECURE:-}
      # Shared with the `tech-step-llm-worker` service below — see
      # requireInternalWorker's doc comment
      # (apps/api/src/middlewares/require-internal-worker.ts). Unset by
      # default: `/internal/tech-steps/*` fails closed rather than open
      # for a deployment that doesn't run the worker at all.
      INTERNAL_WORKER_SECRET: ${INTERNAL_WORKER_SECRET:-}
      # Compose network service name, not localhost — same reasoning as
      # DATABASE_URL above. Unlike INTERNAL_WORKER_SECRET, no `:-` fallback:
      # tech-step-intent-service is a core dependency (see its own entry
      # below), not an optional background job.
      INTENT_SERVICE_BASE_URL: "http://tech-step-intent-service:8000"
      INTENT_SERVICE_SECRET: ${INTENT_SERVICE_SECRET:?set INTENT_SERVICE_SECRET in .env}
    ports:
      - "${APP_PORT:-3000}:3000"
    depends_on:
      postgres:
        condition: service_healthy
      tech-step-intent-service:
        condition: service_healthy

  # spaCy-based NER + intent classification microservice
  # (services/tech-step-intent-service) — `app` delegates all tech-step
  # detection to it over HTTP (see `IntentServiceClient`,
  # apps/api/src/lib/recipe-matching/intent-service-client.ts). Unlike
  # `tech-step-llm-worker` below, **not optional**: without it, `app` can no
  # longer detect any cooking technique in a recipe step at all. No exposed
  # port — reachable only from `app` on the compose network, nothing ever
  # calls into it from outside.
  tech-step-intent-service:
    build:
      context: .
      dockerfile: services/tech-step-intent-service/Dockerfile
    restart: unless-stopped
    environment:
      INTENT_SERVICE_SECRET: ${INTENT_SERVICE_SECRET:?set INTENT_SERVICE_SECRET in .env}
    healthcheck:
      # No curl/wget in the python:3.12-slim base image — a one-line Python
      # request is the healthcheck for a service that's already guaranteed
      # to have Python (see this service's Dockerfile).
      test:
        [
          "CMD",
          "python",
          "-c",
          "import urllib.request; urllib.request.urlopen('http://localhost:8000/health', timeout=2)",
        ]
      interval: 15s
      timeout: 3s
      retries: 5
      # This service trains itself from scratch on every start (no model
      # ever persisted to disk, see its own README) — `/health` only
      # returns 200 once that's done, not just once the base spaCy models
      # are loaded. Measured at ~540s (fr) / ~390s (en), ~930s combined,
      # against the current ~74-technique corpus — each technique now has
      # the *same* number of `utterances` per locale as every other
      # (equalized to the corpus's own pre-existing max, 7/5 — see
      # `training_data.py`'s own doc comment for why a flat, larger target
      # like 20 was tried and reverted) — `start_period` generous enough
      # that failing checks during that whole window never count against
      # `retries` (which would otherwise flip this container to
      # "unhealthy" mid-training, blocking `app`'s own `depends_on:
      # condition: service_healthy` indefinitely).
      start_period: 1200s

  # Deliberately its own image, not built into `app`'s (see
  # services/tech-step-llm-worker/Dockerfile's own doc comment) — a
  # long-lived process with no exposed port (nothing ever calls *into* it,
  # it only ever calls out to `app`). Optional: an `INTERNAL_WORKER_SECRET`-
  # less deployment can omit this service entirely and `app` still runs
  # fine, just without the offline audit/feedback-loop jobs.
  tech-step-llm-worker:
    build:
      context: .
      dockerfile: services/tech-step-llm-worker/Dockerfile
    restart: unless-stopped
    depends_on:
      - app
    environment:
      API_BASE_URL: "http://app:3000"
      INTERNAL_WORKER_SECRET: ${INTERNAL_WORKER_SECRET:?set INTERNAL_WORKER_SECRET in .env to run this service}
      TECH_STEP_WORKER_CRON: ${TECH_STEP_WORKER_CRON:-0 3 * * 0}
    volumes:
      # GGUF weights persist across restarts — see this service's own
      # Dockerfile doc comment on its VOLUME declaration.
      - tech_step_llm_worker_models:/worker/models

volumes:
  postgres_data:
  tech_step_llm_worker_models:
