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: