diff --git a/README.md b/README.md index 53d09be9f9..1432577da3 100644 --- a/README.md +++ b/README.md @@ -135,13 +135,21 @@ heavy parsing, OCR, image captioning, chunking, embedding, and database inserts. It uses the worker defaults from `.env.example` when those vars are set in `.env.local`. These defaults do not prevent live database claims or provider calls. -**Docker Compose also requires connected-workload approval.** -`docker compose up --build` starts both the app and ingestion worker and loads -`.env.local` into both. It does not create an isolated local Supabase database. -Production credentials can therefore let the worker consume production jobs. -Use only the explicitly approved target and workload. The Compose app mapping is -`http://localhost:3000`; stop this stack with `docker compose down`. This mapping -does not replace the project-identity checks for development servers above. +**Docker Compose starts only the app; the worker is opt-in.** +`docker compose --env-file .env.local up --build` builds and starts the app tier +only. Compose has no default Supabase URL: it fails fast unless +`NEXT_PUBLIC_SUPABASE_URL` is supplied (`--env-file`, the shell or `.env`; the +optional `.env.local` `env_file` alone does not feed `${...}` interpolation). +It does not create an isolated local Supabase database. + +The ingestion worker sits behind the `worker` profile because production +credentials would let it consume production jobs. Start it only with explicit +connected-workload approval for the target: +`docker compose --env-file .env.local --profile worker up --build`. +The worker container serves `/health` on internal port 3901 for its Compose +healthcheck (nothing is published). The Compose app mapping is +`http://localhost:3000`; stop the stack with `docker compose --env-file .env.local --profile worker down`. +This mapping does not replace the project-identity checks for development servers above. ### Node.js runtime and lifecycle diff --git a/docker-compose.yml b/docker-compose.yml index 2dac5341ec..b21f5f019c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,17 @@ -version: "3.8" +# Local container stack for the PsychSift app tier. +# +# `docker compose up --build` starts ONLY the app. The ingestion worker sits +# behind the opt-in `worker` profile because it claims jobs from whatever +# Supabase project the credentials point at: with production keys in +# .env.local, an unprofiled worker would start consuming the live ingestion +# queue. Start it only with explicit connected-workload approval: +# docker compose --env-file .env.local --profile worker up --build +# +# NEXT_PUBLIC_SUPABASE_URL has no default on purpose: Compose fails fast when +# it is missing instead of silently building against the production project. +# `${VAR}` interpolation reads the shell and `.env`, not `env_file`, so pass +# the approved values explicitly, e.g.: +# docker compose --env-file .env.local up --build services: # PsychSift Next.js app tier @@ -6,10 +19,13 @@ services: build: context: . dockerfile: Dockerfile - # Build args are optional; pass them only if you need to customize the build args: - NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:-https://sjrfecxgysukkwxsowpy.supabase.co} + NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:?set NEXT_PUBLIC_SUPABASE_URL to the approved Supabase project URL} NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: ${NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY:-placeholder-build-publishable-key} + # Non-secret staging identity: without it the browser-side project + # guard rejects a staging URL as a production mismatch. + NEXT_PUBLIC_SUPABASE_STAGING_PROJECT_REF: ${NEXT_PUBLIC_SUPABASE_STAGING_PROJECT_REF:-} + NEXT_PUBLIC_SUPABASE_STAGING_PROJECT_NAME: ${NEXT_PUBLIC_SUPABASE_STAGING_PROJECT_NAME:-} MAX_UPLOAD_MB: ${MAX_UPLOAD_MB:-} NEXT_PUBLIC_MAX_UPLOAD_MB: ${NEXT_PUBLIC_MAX_UPLOAD_MB:-} ports: @@ -21,11 +37,12 @@ services: # Server-side secrets (never baked into the image) SUPABASE_URL: ${SUPABASE_URL} SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY} - NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL} + NEXT_PUBLIC_SUPABASE_URL: ${NEXT_PUBLIC_SUPABASE_URL:?set NEXT_PUBLIC_SUPABASE_URL to the approved Supabase project URL} NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY: ${NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY} OPENAI_API_KEY: ${OPENAI_API_KEY} env_file: - - .env.local + - path: .env.local + required: false healthcheck: test: [ @@ -41,11 +58,10 @@ services: restart: unless-stopped networks: - psychsift - depends_on: - - worker - # PsychSift Node + Python OCR ingestion worker + # PsychSift Node + Python OCR ingestion worker (opt-in: --profile worker) worker: + profiles: [worker] build: context: . dockerfile: Dockerfile.worker @@ -55,18 +71,34 @@ services: SUPABASE_URL: ${SUPABASE_URL} SUPABASE_SERVICE_ROLE_KEY: ${SUPABASE_SERVICE_ROLE_KEY} OPENAI_API_KEY: ${OPENAI_API_KEY} + # Binds worker/health.ts's /health server inside the container only + # (no published port); used by the healthcheck below. Deliberately not + # a common API port, so sibling-repo port-isolation audits (Medications + # serves its API on the usual 80xx port) do not flag a collision. + WORKER_HEALTH_PORT: "3901" # Worker-specific tuning (from .env.example defaults) WORKER_DOCUMENT_EXTRACTOR_MODE: ${WORKER_DOCUMENT_EXTRACTOR_MODE:-legacy} WORKER_CONCURRENCY: ${WORKER_CONCURRENCY:-2} WORKER_CLAIM_BATCH_SIZE: ${WORKER_CLAIM_BATCH_SIZE:-5} WORKER_STALE_CLAIM_WINDOW_MS: ${WORKER_STALE_CLAIM_WINDOW_MS:-60000} env_file: - - .env.local + - path: .env.local + required: false + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "fetch('http://127.0.0.1:3901/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", + ] + interval: 30s + timeout: 5s + retries: 3 + start_period: 60s restart: unless-stopped networks: - psychsift - # The worker runs long-poll ingestion tasks; no exposed port needed - # but you can add `expose: ['8000']` for internal metrics if added later networks: psychsift: