compose: drive backend/worker/migrate config from .env (env_file)

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) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
This commit is contained in:
2026-06-10 08:46:00 -04:00
parent 519f1c31b5
commit 00f403defa
+27 -48
View File
@@ -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 <no-reply@provenance.local>}
# 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