Skip to content
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
54 changes: 43 additions & 11 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,31 @@
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
app:
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}
Comment thread
BigSimmo marked this conversation as resolved.
Resolved
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:
Expand All @@ -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:
[
Expand All @@ -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
Expand All @@ -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:
Expand Down
Loading