00f403defa
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>
189 lines
7.0 KiB
YAML
189 lines
7.0 KiB
YAML
name: provenance
|
|
|
|
# One stack stands up the whole system. Configuration is entirely env-driven
|
|
# (see .env.example). Run from this directory: `docker compose up -d`.
|
|
#
|
|
# backend/frontend are PULLED from the public registry (git.jpaul.io); CI pushes
|
|
# them to the LAN endpoint (192.168.0.2:1234). For local building instead of
|
|
# pulling, layer the dev override:
|
|
# docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build
|
|
|
|
services:
|
|
postgres:
|
|
# pgvector image = Postgres + pgvector; pg_trgm ships in contrib.
|
|
image: pgvector/pgvector:pg17
|
|
environment:
|
|
POSTGRES_USER: ${POSTGRES_USER:-provenance}
|
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-provenance}
|
|
POSTGRES_DB: ${POSTGRES_DB:-provenance}
|
|
volumes:
|
|
- pgdata:/var/lib/postgresql/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-provenance} -d ${POSTGRES_DB:-provenance}"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 10
|
|
restart: unless-stopped
|
|
|
|
minio:
|
|
image: minio/minio:latest
|
|
command: server /data --console-address ":9001"
|
|
environment:
|
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-provenance}
|
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-change-me-too}
|
|
volumes:
|
|
- miniodata:/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "mc ready local || exit 1"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 10
|
|
restart: unless-stopped
|
|
|
|
# One-shot schema migration: runs `alembic upgrade head` and exits. Backend
|
|
# and worker wait for it to finish, so on `docker compose up` the schema is
|
|
# always current before the app serves traffic — no manual migrate step.
|
|
# NOTE: a pure Watchtower image-swap recreates only the long-running
|
|
# containers, not this one-shot job, so Watchtower deploys should be paired
|
|
# with a `compose up` (see deploy docs) to re-run migrations.
|
|
migrate:
|
|
image: git.jpaul.io/justin/provenance-backend:${IMAGE_TAG:-test-main}
|
|
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:
|
|
DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://provenance:provenance@postgres:5432/provenance}
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
restart: "no"
|
|
|
|
backend:
|
|
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:
|
|
RUN_MIGRATIONS: "1"
|
|
DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://provenance:provenance@postgres:5432/provenance}
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
minio:
|
|
condition: service_healthy
|
|
migrate:
|
|
condition: service_completed_successfully
|
|
healthcheck:
|
|
test:
|
|
- CMD-SHELL
|
|
- >-
|
|
python -c "import urllib.request,sys;
|
|
sys.exit(0 if urllib.request.urlopen('http://localhost:8000/health').status==200 else 1)"
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
start_period: 20s
|
|
restart: unless-stopped
|
|
|
|
# Background worker — same image as the backend, run in worker mode.
|
|
# First job: the scheduled soft-delete purge (and media object cleanup).
|
|
worker:
|
|
image: git.jpaul.io/justin/provenance-backend:${IMAGE_TAG:-test-main}
|
|
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:
|
|
DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://provenance:provenance@postgres:5432/provenance}
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
minio:
|
|
condition: service_healthy
|
|
migrate:
|
|
condition: service_completed_successfully
|
|
restart: unless-stopped
|
|
|
|
frontend:
|
|
image: git.jpaul.io/justin/provenance-frontend:${IMAGE_TAG:-test-main}
|
|
labels:
|
|
com.centurylinklabs.watchtower.enable: "true"
|
|
environment:
|
|
NODE_ENV: production
|
|
depends_on:
|
|
- backend
|
|
restart: unless-stopped
|
|
|
|
caddy:
|
|
image: caddy:2
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
environment:
|
|
# Local default ':80' -> http://localhost. Set to a domain in production
|
|
# for automatic HTTPS (or run plain HTTP behind a Cloudflare Tunnel).
|
|
PROVENANCE_SITE_ADDRESS: ${PROVENANCE_SITE_ADDRESS:-:80}
|
|
volumes:
|
|
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
|
- caddydata:/data
|
|
- caddyconfig:/config
|
|
depends_on:
|
|
- backend
|
|
- frontend
|
|
restart: unless-stopped
|
|
|
|
# Cloudflare Tunnel connector. The tunnel/ingress is configured in the
|
|
# Cloudflare dashboard; this container just connects. One public hostname
|
|
# (e.g. provenance.paul.farm) -> http://caddy:80 is enough, because Caddy
|
|
# does the internal path routing (/ -> frontend, /api + /health -> backend).
|
|
#
|
|
# Opt-in via the "tunnel" profile so local dev doesn't start it. On the lab
|
|
# host set COMPOSE_PROFILES=tunnel so `docker compose up -d` includes it.
|
|
cloudflared:
|
|
image: cloudflare/cloudflared:latest
|
|
restart: unless-stopped
|
|
command: tunnel --no-autoupdate run
|
|
environment:
|
|
TUNNEL_TOKEN: ${CLOUDFLARE_TUNNEL_TOKEN:-}
|
|
depends_on:
|
|
- caddy
|
|
profiles:
|
|
- tunnel
|
|
|
|
# Auto-deploy is handled by the host's global Watchtower (a single
|
|
# nickfedor/watchtower instance watches every container labelled
|
|
# `com.centurylinklabs.watchtower.enable=true` across all stacks). The backend
|
|
# and frontend carry that label above, so a new :test-main image is pulled and
|
|
# the container recreated automatically — no per-stack Watchtower needed.
|
|
|
|
volumes:
|
|
pgdata:
|
|
miniodata:
|
|
caddydata:
|
|
caddyconfig:
|