From 00f403defa84072fe58e260166d366dff2f446d4 Mon Sep 17 00:00:00 2001 From: Justin Paul Date: Wed, 10 Jun 2026 08:46:00 -0400 Subject: [PATCH] compose: drive backend/worker/migrate config from .env (env_file) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the per-setting environment allow-list with `env_file: .env` on the three app-image services, so every setting in app/core/config.py is configurable from .env with no compose edit. This kills the recurring trap where a documented env var (OWNER_EMAIL, the AI keys, SMTP, APP_BASE_URL) silently didn't reach the app because it wasn't on the hand-maintained list. `env_file` is `required: false` so local/CI without a .env still works (falls back to ${VAR:-default} interpolation + code defaults). The small `environment:` block that remains is only for values that must NOT come from .env: - RUN_MIGRATIONS=1 (backend) — a deploy flag, not an app setting. - DATABASE_URL — pinned to the compose-internal host, because the code default points at localhost (wrong inside the network). environment wins over env_file, so this is a safety net if .env ever omits it. Trade-off (accepted, see comment): env_file also injects infra secrets (POSTGRES_*, MINIO_*, CLOUDFLARE_TUNNEL_TOKEN) into the app process env; the app ignores unknown vars (pydantic extra="ignore"). Verified on prod: DATABASE_URL resolves to postgres:5432, RUN_MIGRATIONS=1 and OWNER_EMAIL intact, COOKIE_SECURE=true (no posture change), health 200, trees 200. The earlier explicit AI/SMTP/OWNER passthrough is now subsumed by this. Co-Authored-By: Claude Opus 4.8 (1M context) Signed-off-by: Justin Paul --- deploy/docker-compose.yml | 75 ++++++++++++++------------------------- 1 file changed, 27 insertions(+), 48 deletions(-) diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 5113c12..eda7050 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -51,8 +51,13 @@ services: command: ["uv", "run", "--no-dev", "alembic", "upgrade", "head"] labels: com.centurylinklabs.watchtower.enable: "true" + # All app config comes from .env (twelve-factor) — no per-setting allow-list + # to maintain. The `environment:` block below only pins values that must NOT + # come from .env. See the backend service for the full rationale. + env_file: + - path: .env + required: false environment: - APP_ENV: ${APP_ENV:-development} DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://provenance:provenance@postgres:5432/provenance} depends_on: postgres: @@ -63,50 +68,24 @@ services: image: git.jpaul.io/justin/provenance-backend:${IMAGE_TAG:-test-main} labels: com.centurylinklabs.watchtower.enable: "true" + # Twelve-factor: ALL application settings come straight from .env — owner, + # AI providers, mailer/SMTP, S3, sessions, everything in app/core/config.py. + # No per-setting allow-list to maintain, so a new setting in .env (and + # .env.example) reaches the app with no compose edit. The `environment:` + # block below is only for values that must NOT come from .env: + # - RUN_MIGRATIONS: backend-only flag, not an app setting. + # - DATABASE_URL: pinned to the compose-internal host as a safety net — + # the code default points at localhost, which is wrong inside the + # network. (.env normally sets it; this guards against it being absent.) + # `environment:` wins over `env_file`, so these always take effect. + # Trade-off (accepted): env_file also exposes infra secrets (POSTGRES_*, + # MINIO_*, CLOUDFLARE_TUNNEL_TOKEN) to the app process; the app ignores them. + env_file: + - path: .env + required: false environment: - APP_ENV: ${APP_ENV:-development} - # Self-migrate on start so a Watchtower in-place image swap applies any new - # migrations (idempotent). The one-shot `migrate` service covers the same - # for `compose up`; the depends_on below serializes them so they never run - # alembic concurrently. RUN_MIGRATIONS: "1" DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://provenance:provenance@postgres:5432/provenance} - # Instance owner/operator — the account(s) with instance-admin rights. - OWNER_EMAIL: ${OWNER_EMAIL:-} - S3_ENDPOINT_URL: ${S3_ENDPOINT_URL:-http://minio:9000} - S3_BUCKET: ${S3_BUCKET:-provenance} - S3_ACCESS_KEY: ${S3_ACCESS_KEY:-provenance} - S3_SECRET_KEY: ${S3_SECRET_KEY:-change-me-too} - S3_REGION: ${S3_REGION:-us-east-1} - # Email / mailer — verification + password-reset links. APP_BASE_URL is the - # base for those links; MAILER=smtp activates the SMTP_* settings. - APP_BASE_URL: ${APP_BASE_URL:-http://localhost} - REQUIRE_EMAIL_VERIFICATION: ${REQUIRE_EMAIL_VERIFICATION:-false} - MAILER: ${MAILER:-console} - SMTP_HOST: ${SMTP_HOST:-} - SMTP_PORT: ${SMTP_PORT:-587} - SMTP_USERNAME: ${SMTP_USERNAME:-} - SMTP_PASSWORD: ${SMTP_PASSWORD:-} - SMTP_FROM: ${SMTP_FROM:-Provenance } - # Model providers (AI assistant + embeddings). Each activates when its key - # is set; DEFAULT_*_PROVIDER picks the default. 'null' keeps AI off. - DEFAULT_LLM_PROVIDER: ${DEFAULT_LLM_PROVIDER:-null} - DEFAULT_EMBEDDING_PROVIDER: ${DEFAULT_EMBEDDING_PROVIDER:-null} - LLM_MAX_TOKENS: ${LLM_MAX_TOKENS:-4096} - EMBEDDING_DIMENSIONS: ${EMBEDDING_DIMENSIONS:-1536} - ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} - ANTHROPIC_MODEL: ${ANTHROPIC_MODEL:-claude-opus-4-8} - OPENAI_API_KEY: ${OPENAI_API_KEY:-} - OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1} - OPENAI_MODEL: ${OPENAI_MODEL:-gpt-4o} - OPENAI_EMBEDDING_MODEL: ${OPENAI_EMBEDDING_MODEL:-text-embedding-3-small} - XAI_API_KEY: ${XAI_API_KEY:-} - XAI_BASE_URL: ${XAI_BASE_URL:-https://api.x.ai/v1} - XAI_MODEL: ${XAI_MODEL:-grok-2-latest} - OLLAMA_ENABLED: ${OLLAMA_ENABLED:-false} - OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://localhost:11434/v1} - OLLAMA_MODEL: ${OLLAMA_MODEL:-llama3.1} - OLLAMA_EMBEDDING_MODEL: ${OLLAMA_EMBEDDING_MODEL:-nomic-embed-text} depends_on: postgres: condition: service_healthy @@ -133,14 +112,14 @@ services: command: ["uv", "run", "--no-dev", "python", "-m", "app.worker"] labels: com.centurylinklabs.watchtower.enable: "true" + # Same .env-driven config as the backend (see its comment). The worker reads + # the model-provider settings too, so the upcoming embedding/matching jobs + # are configured the moment they land — no compose change needed. + env_file: + - path: .env + required: false environment: - APP_ENV: ${APP_ENV:-development} DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://provenance:provenance@postgres:5432/provenance} - S3_ENDPOINT_URL: ${S3_ENDPOINT_URL:-http://minio:9000} - S3_BUCKET: ${S3_BUCKET:-provenance} - S3_ACCESS_KEY: ${S3_ACCESS_KEY:-provenance} - S3_SECRET_KEY: ${S3_SECRET_KEY:-change-me-too} - S3_REGION: ${S3_REGION:-us-east-1} depends_on: postgres: condition: service_healthy -- 2.52.0