From 13bed436422b1441d03fa35f1952e1a1384b0171 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:28:24 +0800 Subject: [PATCH 1/5] fix(compose): make the worker opt-in and require an explicit Supabase URL A local `docker compose up` with production keys started a worker on the live ingestion queue, and the app build silently defaulted NEXT_PUBLIC_SUPABASE_URL to the production project. - Remove the production Supabase URL default; Compose now fails fast unless NEXT_PUBLIC_SUPABASE_URL is supplied. - Put the worker behind profiles: [worker] and drop the app's depends_on: worker. - Remove the obsolete version: "3.8" key. - Load .env.local via env_file with required: false. - Bind the worker's /health server on internal port 8080 and add a Compose healthcheck for it. - Update the README Compose section to match. --- README.md | 22 ++++++++++++++------- docker-compose.yml | 48 +++++++++++++++++++++++++++++++++++----------- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 53d09be9f9..7c92eaac26 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 8080 for its Compose +healthcheck (nothing is published). The Compose app mapping is +`http://localhost:3000`; stop the stack with `docker compose --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..7eba8e2702 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 --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,9 +19,8 @@ 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} MAX_UPLOAD_MB: ${MAX_UPLOAD_MB:-} NEXT_PUBLIC_MAX_UPLOAD_MB: ${NEXT_PUBLIC_MAX_UPLOAD_MB:-} @@ -21,11 +33,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 +54,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 +67,32 @@ 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. + WORKER_HEALTH_PORT: "8080" # 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:8080/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: From 00ff64ecddab39f78055fe2c5a4f665387affc64 Mon Sep 17 00:00:00 2001 From: "qodo-code-review[bot]" <151058649+qodo-code-review[bot]@users.noreply.github.com> Date: Sun, 18 Oct 2026 09:31:19 +0000 Subject: [PATCH 2/5] fix: Pass the environment file when stopping --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c92eaac26..8efb116a63 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ connected-workload approval for the target: `docker compose --env-file .env.local --profile worker up --build`. The worker container serves `/health` on internal port 8080 for its Compose healthcheck (nothing is published). The Compose app mapping is -`http://localhost:3000`; stop the stack with `docker compose --profile worker down`. +`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 From d23153df989842e09b8f18bf3dcde1a60898b9ff Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:33:28 +0800 Subject: [PATCH 3/5] fix(compose): use internal port 3901 for the worker health server 8080 is Medications' API port, so sibling-repo port-isolation audits flagged the Compose worker health port as a collision. --- README.md | 2 +- docker-compose.yml | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7c92eaac26..a08acd357d 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ 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 8080 for its Compose +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 --profile worker down`. This mapping does not replace the project-identity checks for development servers above. diff --git a/docker-compose.yml b/docker-compose.yml index 7eba8e2702..4aff34c1a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -68,8 +68,10 @@ services: 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. - WORKER_HEALTH_PORT: "8080" + # (no published port); used by the healthcheck below. 3901 rather than + # 8080 so sibling-repo port-isolation audits (Medications uses 8080) + # 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} @@ -84,7 +86,7 @@ services: "CMD", "node", "-e", - "fetch('http://127.0.0.1:8080/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", + "fetch('http://127.0.0.1:3901/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))", ] interval: 30s timeout: 5s From b8220df0446c89053b72b8c02e88050431b756fd Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:33:46 +0800 Subject: [PATCH 4/5] docs(compose): drop the literal colliding port from the health-port comment --- docker-compose.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4aff34c1a5..742a4d4d73 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -68,9 +68,9 @@ services: 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. 3901 rather than - # 8080 so sibling-repo port-isolation audits (Medications uses 8080) - # do not flag a collision. + # (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} From 4c1aba55aa937839726aad7430da3f34b48eaddb Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:50:19 +0800 Subject: [PATCH 5/5] fix(compose): forward staging identity build args; env-file in worker example - Pass NEXT_PUBLIC_SUPABASE_STAGING_PROJECT_REF/NAME build args so the browser project guard accepts an explicitly configured staging URL. - Use --env-file .env.local in the header's worker startup example. --- docker-compose.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 742a4d4d73..b21f5f019c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ # 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 --profile worker up --build +# 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. @@ -22,6 +22,10 @@ services: args: 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: