name: loreal-prod-tracker services: # ─── PostgreSQL with pgvector ─────────────────────────── db: image: pgvector/pgvector:pg17 restart: unless-stopped environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres} POSTGRES_DB: loreal_prod_tracker # Host port is overridable via DB_HOST_PORT env var — deploy.sh auto-picks # a free one if 5492 is taken on the host. The container-internal port # (5432) never changes — the app connects to db:5432 over the Docker # network and doesn't care what host port (if any) is mapped. ports: - "${DB_HOST_PORT:-5492}:5432" volumes: - pgdata:/var/lib/postgresql/data - ./docker/db-init.sql:/docker-entrypoint-initdb.d/01-pgvector.sql:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s timeout: 5s retries: 5 # ─── Next.js app ─────────────────────────────────────── app: build: context: . dockerfile: Dockerfile restart: unless-stopped # Host port is overridable via APP_HOST_PORT env var — deploy.sh auto-picks # a free one if 3002 is taken, and writes the chosen port into the Apache # reverse-proxy config (apache/loreal-prod-tracker.conf) at the same time. ports: - "${APP_HOST_PORT:-3002}:3000" environment: # DATABASE_URL tuning knobs matter at ~40 concurrent users: # connection_limit — how many pooled connections Prisma will # open per app instance. Default is cpus*2+1 (~5-9 inside a # container), which can run out at peak when mutations + query # polling coincide. 20 gives plenty of headroom for this scale. # pool_timeout — seconds to wait for a free connection before # failing the request (default 10). 10s matches the default # and stays explicit. # Postgres side: default max_connections is 100, so 20 × a few # app replicas is well below the ceiling. DATABASE_URL: postgresql://postgres:${DB_PASSWORD:-postgres}@db:5432/loreal_prod_tracker?schema=public&connection_limit=20&pool_timeout=10 # Ollama — points to internal GPU server for embeddings + chat fallback OLLAMA_HOST: ${OLLAMA_HOST:-http://10.24.42.219:11434} OLLAMA_CHAT_HOST: ${OLLAMA_CHAT_HOST:-http://10.24.42.219:11434} OLLAMA_CHAT_MODEL: ${OLLAMA_CHAT_MODEL:-gemma4:latest} OLLAMA_EMBED_MODEL: ${OLLAMA_EMBED_MODEL:-nomic-embed-text} NODE_ENV: production AUTH_SECRET: ${AUTH_SECRET} AUTH_TRUST_HOST: "true" # Azure SPA registration — PKCE in browser, no client secret AZURE_CLIENT_ID: ${AZURE_CLIENT_ID} AZURE_TENANT_ID: ${AZURE_TENANT_ID} AZURE_REDIRECT_URI: ${AZURE_REDIRECT_URI:-} CRON_SECRET: ${CRON_SECRET:-change-me} API_KEY: ${API_KEY:-} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-} DEV_BYPASS_AUTH: ${DEV_BYPASS_AUTH:-false} DEV_USER_ID: ${DEV_USER_ID:-} # OMG webhook (Shashank pending — stub until payload confirmed) OMG_WEBHOOK_SECRET: ${OMG_WEBHOOK_SECRET:-} OMG_WEBHOOK_ALLOW_INSECURE: ${OMG_WEBHOOK_ALLOW_INSECURE:-false} # Auth: Entra SSO stays coded but gated. Flip to "true" post-MVP once redirect URI is live. NEXT_PUBLIC_AUTH_ENTRA_ENABLED: ${NEXT_PUBLIC_AUTH_ENTRA_ENABLED:-false} # Box integration (Phase 4). BOX_CONFIG_FILE points at the mounted # JSON secret below. The other vars come from .env on the host. BOX_CONFIG_FILE: ${BOX_CONFIG_FILE:-/run/secrets/box-config.json} BOX_OUT_FOLDER_ID: ${BOX_OUT_FOLDER_ID:-} BOX_WATCH_FOLDER_ID: ${BOX_WATCH_FOLDER_ID:-} BOX_WEBHOOK_PRIMARY_KEY: ${BOX_WEBHOOK_PRIMARY_KEY:-} BOX_WEBHOOK_SECONDARY_KEY: ${BOX_WEBHOOK_SECONDARY_KEY:-} BOX_WEBHOOK_ALLOW_INSECURE: ${BOX_WEBHOOK_ALLOW_INSECURE:-false} volumes: - uploads_data:/data/uploads secrets: # Long-form so the secret lands at `/run/secrets/box-config.json` — # matching BOX_CONFIG_FILE above. The default short-form would mount # at `/run/secrets/box-config` (no extension), causing isBoxConfigured() # to read from the wrong path. - source: box-config target: box-config.json depends_on: db: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/api/health || exit 1"] interval: 15s timeout: 5s retries: 3 start_period: 30s volumes: pgdata: uploads_data: # Box JWT app config. Drop the JSON downloaded from the Box developer # console at ./secrets/box-config.json on the host before `docker compose # up`. Until that file exists, the app starts but Box features are # disabled (isBoxConfigured() returns false; UI hides "Send to client"). secrets: box-config: file: ./secrets/box-config.json