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} 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: