Files
provenance/deploy/docker-compose.yml
T
justin 3a1395b6af compose: forward AI provider + mailer/SMTP env to the backend
Follow-up to the OWNER_EMAIL passthrough. The backend service env block is an
explicit allow-list, so the documented model-provider keys (ANTHROPIC_*,
OPENAI_*, XAI_*, OLLAMA_*, DEFAULT_*_PROVIDER, LLM_MAX_TOKENS,
EMBEDDING_DIMENSIONS) and mailer settings (MAILER, SMTP_*, APP_BASE_URL,
REQUIRE_EMAIL_VERIFICATION) never reached the container — setting them in .env
was a no-op. The AI assistant/policy and the SMTP mailer run in the backend, so
forward them here.

Side fix: APP_BASE_URL was likewise dropped, so outbound email links used the
code default http://localhost instead of the configured domain. Now forwarded
(verified live: backend reports APP_BASE_URL=https://provenance.paul.farm).

Worker is left as-is (it consumes neither today); it'll need the model vars when
embedding/matching jobs land. Alternative to this growing allow-list is
`env_file: .env` on the service — deferred to avoid forwarding unrelated secrets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
2026-06-10 08:38:49 -04:00

210 lines
8.1 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"
environment:
APP_ENV: ${APP_ENV:-development}
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"
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
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"
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
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: