# briven — Dokploy-managed compose (generated from compose.yml). # Dokploy controls the project name + container names, so this file omits # the top-level `name:` and per-service `container_name:` that compose.yml # pins for the raw self-host path. Keep this in sync with compose.yml. # Source of truth for the engine/services is compose.yml (DoltGres data plane). # briven — canonical self-host compose (build-from-source). # # Product line (flndrn 2026-07-21): Briven is Doltgres end-to-end. # # - control plane = DoltGres database `briven_control` on the doltgres service # (sign-in, orgs, projects, billing, Briven Auth). BRIVEN_DATABASE_URL. # - data plane = same DoltGres cluster, one DATABASE per customer project # (`proj_…`). BRIVEN_DATA_PLANE_URL. Realtime polls DOLT_HASHOF('HEAD'). # # Stock Postgres (pgvector) may still exist on the host for rollback only — # live traffic must not use it. Do not reintroduce control-on-Postgres. # # This file supersedes the two earlier conflicting composes: # - the old Dolt-MySQL build (dolthub/dolt-sql-server + BRIVEN_URL mysql) — # wrong engine, broke the api. REMOVED. # - the all-pgvector build (data plane as a 2nd Postgres DB) — wrong data # plane. REPLACED by the real DoltGres service below. # # Dokploy clones the repo and runs `docker compose build` against the local # Dockerfiles — no external registry, no GHCR, no docker.sock. Per # docs/DOCKER.md §7 / infra/CLAUDE.md: every long-running service caps its # log volume via the *briven-logging anchor; no watchtower, no docker_sd, # no registry polling on the host. # # Single-machine layout, ~25 concurrent customer projects. Past that, split # the control plane onto one host and the data plane (doltgres + minio) onto # another. # # Required env (drop a `.env` next to this file — see .env.example): # # BRIVEN_DOMAIN e.g. briven.example.com # BRIVEN_BETTER_AUTH_SECRET openssl rand -hex 32 # BRIVEN_AUDIT_IP_PEPPER openssl rand -hex 32 # BRIVEN_ENCRYPTION_KEY openssl rand -hex 32 # BRIVEN_RUNTIME_SHARED_SECRET openssl rand -hex 32 # BRIVEN_POSTGRES_PASSWORD control-plane Postgres superuser password # BRIVEN_DOLTGRES_PASSWORD data-plane DoltGres superuser password # BRIVEN_MINIO_ROOT_PASSWORD MinIO root / S3 secret key # (optional) BRIVEN_MITTERA_*, BRIVEN_*_CLIENT_ID/SECRET, BRIVEN_POLAR_*, # BRIVEN_OLLAMA_*, BRIVEN_MINIO_BUCKET/REGION, BRIVEN_OPEN_SIGNUPS # # After first boot: # 1. Create the first user via the magic-link flow on https://${BRIVEN_DOMAIN} # 2. Promote to admin in the control plane: # docker exec -it briven-postgres psql -U postgres -d briven_control \ # -c "UPDATE users SET is_admin = true WHERE id = '...';" # 3. Create your first project via the dashboard (provisions a DoltGres DB). x-logging: &briven-logging driver: json-file options: max-size: '10m' max-file: '3' services: # ─── control plane ──────────────────────────────────────────────────── postgres: image: pgvector/pgvector:pg17 restart: unless-stopped logging: *briven-logging environment: POSTGRES_PASSWORD: ${BRIVEN_POSTGRES_PASSWORD} POSTGRES_DB: briven_control volumes: - postgres_data:/var/lib/postgresql/data # Control-plane init only: enables pgvector + pg_trgm on briven_control. # No data-plane DB is created here — the data plane is the doltgres # service, with a database per project (see ADR-0002). - ./postgres-init:/docker-entrypoint-initdb.d:ro healthcheck: test: ['CMD-SHELL', 'pg_isready -U postgres -d briven_control'] interval: 10s timeout: 5s retries: 5 start_period: 20s networks: - briven labels: - 'briven_logs=true' # ─── data plane ─────────────────────────────────────────────────────── # DoltGres = Postgres-wire, git-for-data. Each customer project is its own # DATABASE here, created by the api over the `pg` driver. The default # superuser/database is `postgres`/`postgres` (DOLTGRES_* envs override the # password). # # IMAGE IS PINNED BY DIGEST, ON PURPOSE (2026-07-07 maintenance window): # `:latest` let a deploy silently swap the database engine under live data # (prime suspect in the 2026-07-07 auth.db outage). To upgrade the engine, # change the digest here deliberately, in its own reviewed deploy. # # DATA DIR IS /var/lib/doltgres — NO "ql". The mount below once pointed at # /var/lib/doltgresql (typo), so all real data lived in an anonymous volume # that a container recreation would orphan. Fixed 2026-07-07 (data migrated # into the named volume during the maintenance window). Never change this # path without checking `config.yaml` inside the volume. doltgres: image: dolthub/doltgresql:0.57.2 restart: unless-stopped logging: *briven-logging environment: DOLTGRES_USER: postgres DOLTGRES_PASSWORD: ${BRIVEN_DOLTGRES_PASSWORD} volumes: - doltgres_data:/var/lib/doltgres # Dolt-native backups land here (written by the server itself); the host # systemd job infra/backups/briven-backup.sh triggers them (gentle, one # DB at a time). The old in-compose dolt-backup sidecar was REMOVED # 2026-08-01 — its un-throttled per-DB loop helped lock DoltGres under load. - doltgres_backups:/backups healthcheck: # pg_isready ships in the doltgresql image and needs no password. test: ['CMD-SHELL', 'pg_isready -h 127.0.0.1 -p 5432 -U postgres'] interval: 10s timeout: 5s retries: 5 start_period: 30s networks: - briven labels: - 'briven_logs=true' # ─── data-plane backup: moved to the host systemd job (2026-08-01) ────── # The in-compose `dolt-backup` sidecar was REMOVED. Its un-throttled loop of # `dolt_backup('sync-url', …)` over every DB helped lock DoltGres 0.56.6 under # load (full-platform outage, see docs/knowledge-base.md → INCIDENT 2026-08-01). # Backups now run from infra/backups/briven-backup.sh (systemd timer): a GENTLE, # one-DB-at-a-time, throttled `dolt_backup` pass on the fixed 0.57.2 engine, # plus off-site mirror. Restorable via `dolt backup restore`. # # NOTE (still true): `pg_dump` CANNOT back up the DoltGres data plane — it opens # a REPEATABLE READ READ ONLY snapshot txn DoltGres doesn't implement # ("SET TRANSACTION is not yet supported"). Dolt-native `dolt_backup` is the # only data-plane backup path; pg_dump is a stock-Postgres control-plane helper. # ─── briven-engine Auth DB (DOLTGRES ONLY — non-negotiable) ─────────── # HARD RULE: the COMPLETE Briven project is Doltgres. New parts do not get # a separate stock-Postgres brain. SuperTokens Core Docker is REMOVED — # it is incompatible with Doltgres (SET SESSION CHARACTERISTICS). # briven-engine = Briven API code + tables in Doltgres DB `briven_engine`. # Create/migrate: API `ensureBrivenEngineDatabase` + schema bootstrap. briven-engine-db-init: image: dolthub/doltgresql:0.57.2 restart: 'no' logging: *briven-logging depends_on: doltgres: condition: service_healthy environment: PGPASSWORD: ${BRIVEN_DOLTGRES_PASSWORD} entrypoint: - /bin/sh - -c - | set -e echo "briven-engine-db-init: ensuring briven_engine on DOLTGRES only…" exists=$$(psql -h doltgres -U postgres -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname='briven_engine'" || true) if [ "$$exists" = "1" ]; then echo "briven_engine already exists on doltgres" else psql -h doltgres -U postgres -d postgres -c 'CREATE DATABASE briven_engine' echo "created briven_engine on doltgres" fi networks: - briven labels: - 'briven.service=briven-engine-db-init' - 'briven.db=doltgres' # ─── shared infra ───────────────────────────────────────────────────── redis: image: redis:7.4-alpine restart: unless-stopped logging: *briven-logging command: redis-server --appendonly yes volumes: - redis_data:/data healthcheck: test: ['CMD-SHELL', 'redis-cli ping | grep -q PONG'] interval: 10s timeout: 5s retries: 5 start_period: 10s networks: - briven labels: - 'briven_logs=true' minio: image: minio/minio:latest restart: unless-stopped logging: *briven-logging command: server /data --console-address ':9001' environment: MINIO_ROOT_USER: briven MINIO_ROOT_PASSWORD: ${BRIVEN_MINIO_ROOT_PASSWORD} volumes: - minio_data:/data healthcheck: test: ['CMD-SHELL', 'curl -fsS http://localhost:9000/minio/health/live || exit 1'] interval: 15s timeout: 5s retries: 5 start_period: 20s networks: - briven - dokploy-network labels: - 'briven_logs=true' - 'traefik.enable=true' - 'traefik.docker.network=dokploy-network' # Public S3 endpoint — browsers PUT/GET with sigv4-presigned URLs the # api mints. The api also reaches MinIO internally at http://minio:9000. - 'traefik.http.routers.briven-s3.rule=Host(`s3.${BRIVEN_DOMAIN}`)' - 'traefik.http.routers.briven-s3.entrypoints=websecure' - 'traefik.http.routers.briven-s3.tls.certresolver=letsencrypt' - 'traefik.http.routers.briven-s3.service=briven-s3' - 'traefik.http.services.briven-s3.loadbalancer.server.port=9000' # One-shot bucket creator. `mc mb --ignore-existing` is idempotent, so this # runs every deploy and no-ops after the first. restart: 'no' = one-shot, so # per infra/CLAUDE.md it does NOT need the logging cap. minio-init: image: minio/mc:latest depends_on: minio: condition: service_healthy entrypoint: > /bin/sh -c " until /usr/bin/mc alias set minio http://minio:9000 briven ${BRIVEN_MINIO_ROOT_PASSWORD} >/dev/null 2>&1; do echo 'waiting for minio...'; sleep 2; done; /usr/bin/mc mb --ignore-existing minio/${BRIVEN_MINIO_BUCKET:-briven}; echo 'minio bucket ready: ${BRIVEN_MINIO_BUCKET:-briven}'; " restart: 'no' networks: - briven # ─── application services ───────────────────────────────────────────── api: build: context: ../.. dockerfile: apps/api/Dockerfile restart: unless-stopped logging: *briven-logging depends_on: postgres: condition: service_healthy doltgres: condition: service_healthy redis: condition: service_healthy briven-engine-db-init: condition: service_completed_successfully environment: BRIVEN_ENV: production BRIVEN_API_PORT: '3001' BRIVEN_API_ORIGIN: https://api.${BRIVEN_DOMAIN} BRIVEN_WEB_ORIGIN: https://${BRIVEN_DOMAIN} BRIVEN_ADMIN_ORIGIN: https://admin.${BRIVEN_DOMAIN} # briven-engine = API + Doltgres only (NO SuperTokens Core container). BRIVEN_AUTH_CORE_ENABLED: ${BRIVEN_AUTH_CORE_ENABLED:-true} # SQL for Auth vault — ALWAYS Doltgres DB briven_engine (DOLTGRES-FIRST). BRIVEN_ENGINE_DATABASE_URL: postgres://postgres:${BRIVEN_DOLTGRES_PASSWORD}@doltgres:5432/briven_engine?sslmode=disable BRIVEN_AUTH_ENABLED: ${BRIVEN_AUTH_ENABLED:-false} # Observability stack (host-managed compose at /root/briven-observability # on the France box, same dokploy-network) — powers admin host gauges, # the live cpu chart, and /v1/admin/timeseries prom-backed series. BRIVEN_PROMETHEUS_URL: ${BRIVEN_PROMETHEUS_URL:-} # Hard allowlist: only these emails can EVER be platform admin # (see apps/api/src/lib/superadmin.ts). Value lives in the Dokploy env. BRIVEN_SUPERADMIN_EMAILS: ${BRIVEN_SUPERADMIN_EMAILS:-} BRIVEN_TRUSTED_ORIGINS: https://${BRIVEN_DOMAIN},https://app.${BRIVEN_DOMAIN},https://api.${BRIVEN_DOMAIN},https://admin.${BRIVEN_DOMAIN} # Control plane — stock Postgres, postgres.js/drizzle. # Control brain on Doltgres (same cluster as project DBs). Password is # DOLTGRES, not the legacy stock-Postgres password. BRIVEN_DATABASE_URL: postgres://postgres:${BRIVEN_DOLTGRES_PASSWORD}@doltgres:5432/briven_control?sslmode=disable # Data plane — DoltGres, `pg` driver, database-per-project. The api # connects to the default `postgres` database and CREATEs per-project # databases on this server. BRIVEN_DATA_PLANE_URL: postgres://postgres:${BRIVEN_DOLTGRES_PASSWORD}@doltgres:5432/postgres?sslmode=disable BRIVEN_REDIS_URL: redis://redis:6379 BRIVEN_RUNTIME_URL: http://runtime:3003 # Without this the api falls back to localhost:3004 and can't reach # realtime — surfaces as realtime_stats_failed "Unable to connect". BRIVEN_REALTIME_URL: http://realtime:3004 BRIVEN_RUNTIME_SHARED_SECRET: ${BRIVEN_RUNTIME_SHARED_SECRET} BRIVEN_BETTER_AUTH_SECRET: ${BRIVEN_BETTER_AUTH_SECRET} BRIVEN_AUDIT_IP_PEPPER: ${BRIVEN_AUDIT_IP_PEPPER} BRIVEN_ENCRYPTION_KEY: ${BRIVEN_ENCRYPTION_KEY} # Per-tenant secret-store master key (ARCHITECTURE.md §4/§9). # BRIVEN_AUTH_ENABLED is set once above with the engine env block. # Values live in the Dokploy env panel; empty = unset in loadEnv. BRIVEN_AUTH_MASTER_KEY: ${BRIVEN_AUTH_MASTER_KEY:-} BRIVEN_MITTERA_API_URL: ${BRIVEN_MITTERA_API_URL:-} BRIVEN_MITTERA_API_KEY: ${BRIVEN_MITTERA_API_KEY:-} BRIVEN_MITTERA_WEBHOOK_SECRET: ${BRIVEN_MITTERA_WEBHOOK_SECRET:-} # Auth OTP / magic-link real inbox: set HOST+USER+PASS(+FROM) for SMTP primary. # Until SMTP is set, Auth uses mittera (same as platform mail). BRIVEN_SMTP_HOST: ${BRIVEN_SMTP_HOST:-} BRIVEN_SMTP_PORT: ${BRIVEN_SMTP_PORT:-587} BRIVEN_SMTP_USER: ${BRIVEN_SMTP_USER:-} BRIVEN_SMTP_PASS: ${BRIVEN_SMTP_PASS:-} BRIVEN_SMTP_FROM: ${BRIVEN_SMTP_FROM:-} BRIVEN_GOOGLE_CLIENT_ID: ${BRIVEN_GOOGLE_CLIENT_ID:-} BRIVEN_GOOGLE_CLIENT_SECRET: ${BRIVEN_GOOGLE_CLIENT_SECRET:-} BRIVEN_GITHUB_CLIENT_ID: ${BRIVEN_GITHUB_CLIENT_ID:-} BRIVEN_GITHUB_CLIENT_SECRET: ${BRIVEN_GITHUB_CLIENT_SECRET:-} BRIVEN_KONNOS_CLIENT_ID: ${BRIVEN_KONNOS_CLIENT_ID:-} BRIVEN_KONNOS_CLIENT_SECRET: ${BRIVEN_KONNOS_CLIENT_SECRET:-} BRIVEN_KONNOS_ISSUER: ${BRIVEN_KONNOS_ISSUER:-https://code.konnos.org} BRIVEN_DISCORD_CLIENT_ID: ${BRIVEN_DISCORD_CLIENT_ID:-} BRIVEN_DISCORD_CLIENT_SECRET: ${BRIVEN_DISCORD_CLIENT_SECRET:-} BRIVEN_POLAR_API_BASE: ${BRIVEN_POLAR_API_BASE:-https://api.polar.sh} BRIVEN_POLAR_ACCESS_TOKEN: ${BRIVEN_POLAR_ACCESS_TOKEN:-} BRIVEN_POLAR_WEBHOOK_SECRET: ${BRIVEN_POLAR_WEBHOOK_SECRET:-} BRIVEN_POLAR_PRO_PRODUCT_ID: ${BRIVEN_POLAR_PRO_PRODUCT_ID:-} BRIVEN_POLAR_TEAM_PRODUCT_ID: ${BRIVEN_POLAR_TEAM_PRODUCT_ID:-} BRIVEN_DOMAIN: ${BRIVEN_DOMAIN} BRIVEN_OPEN_SIGNUPS: ${BRIVEN_OPEN_SIGNUPS:-false} BRIVEN_OLLAMA_URL: ${BRIVEN_OLLAMA_URL:-} BRIVEN_OLLAMA_API_KEY: ${BRIVEN_OLLAMA_API_KEY:-} BRIVEN_OLLAMA_MODEL: ${BRIVEN_OLLAMA_MODEL:-qwen2.5-coder:32b} BRIVEN_MINIO_ENDPOINT: http://minio:9000 BRIVEN_MINIO_PUBLIC_ENDPOINT: https://s3.${BRIVEN_DOMAIN} BRIVEN_MINIO_ACCESS_KEY: briven BRIVEN_MINIO_SECRET_KEY: ${BRIVEN_MINIO_ROOT_PASSWORD} BRIVEN_MINIO_BUCKET: ${BRIVEN_MINIO_BUCKET:-briven} BRIVEN_MINIO_REGION: ${BRIVEN_MINIO_REGION:-us-east-1} # imgproxy — on-the-fly image transforms (M4). The api mints SIGNED # imgproxy URLs (services/image-transform.ts). ENDPOINT is the # media host + the /_t path prefix (imgproxy sits BEHIND media. # under PathPrefix(/_t), no new subdomain). image-transform.ts signs the # path WITHOUT the prefix (/rs:.../g:sm/) and imgproxy strips # /_t before verifying — the signed portions match. KEY/SALT are the same # hex secrets the imgproxy container gets; set all three in the Dokploy # env to turn transforms on (unset => endpoint returns 503, fail-safe). BRIVEN_IMGPROXY_ENDPOINT: https://media.${BRIVEN_DOMAIN}/_t BRIVEN_IMGPROXY_KEY: ${BRIVEN_IMGPROXY_KEY:-} BRIVEN_IMGPROXY_SALT: ${BRIVEN_IMGPROXY_SALT:-} # Self-hosted MaxMind GeoLite2-City for Auth email Location (city/country). # Host file: /var/lib/GeoIP/GeoLite2-City.mmdb (bind-mounted below). BRIVEN_GEOIP_DB_PATH: ${BRIVEN_GEOIP_DB_PATH:-/var/lib/GeoIP/GeoLite2-City.mmdb} volumes: # Read-only city DB for Auth email geo (device location line). - /var/lib/GeoIP:/var/lib/GeoIP:ro healthcheck: # /info is documented to never 500 (apps/api/Dockerfile). bun ships in # the api image and has a built-in fetch. test: ['CMD-SHELL', "bun -e \"fetch('http://localhost:3001/info').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""] interval: 15s timeout: 5s retries: 5 start_period: 40s networks: - briven - dokploy-network labels: - 'briven_logs=true' - 'traefik.enable=true' - 'traefik.docker.network=dokploy-network' - 'traefik.http.routers.briven-api.rule=Host(`api.${BRIVEN_DOMAIN}`)' - 'traefik.http.routers.briven-api.entrypoints=websecure' - 'traefik.http.routers.briven-api.tls.certresolver=letsencrypt' - 'traefik.http.routers.briven-api.service=briven-api' - 'traefik.http.services.briven-api.loadbalancer.server.port=3001' # media.briven.tech -> this api (public file bytes). Explicit .service= on # every router so Traefik does not treat the container as multi-service. - 'traefik.http.routers.briven-media.rule=Host(`media.${BRIVEN_DOMAIN}`)' - 'traefik.http.routers.briven-media.entrypoints=websecure' - 'traefik.http.routers.briven-media.tls.certresolver=letsencrypt' - 'traefik.http.routers.briven-media.service=briven-api' # /_t image transforms: router lives on the imgproxy service (below), not here. # imgproxy — on-the-fly image transforms (M4). Sits BEHIND media. # under the /_t path prefix (no new public subdomain, per the media-host # rule). The api mints signed URLs; imgproxy fetches the source from the # PUBLIC media host, resizes, and returns it. SSRF is locked to the media # host via IMGPROXY_ALLOWED_SOURCES. No host port — only Traefik (on the # dokploy-network) can reach port 8080. imgproxy: image: darthsim/imgproxy:latest restart: unless-stopped logging: *briven-logging environment: # Signing — MUST equal the api's BRIVEN_IMGPROXY_KEY/SALT (same hex # secrets, set in the Dokploy env). imgproxy strips IMGPROXY_PATH_PREFIX # (/_t) from the request path, then verifies the HMAC over the remaining # /rs:.../g:sm/ — exactly what image-transform.ts signed. IMGPROXY_KEY: ${BRIVEN_IMGPROXY_KEY:-} IMGPROXY_SALT: ${BRIVEN_IMGPROXY_SALT:-} IMGPROXY_PATH_PREFIX: /_t # SSRF guard: imgproxy may ONLY fetch sources from the public media host. IMGPROXY_ALLOWED_SOURCES: https://media.${BRIVEN_DOMAIN}/ # Sane limits — resize-bomb / oversized-source guards. Format auto-nego: # serve WebP to browsers that send Accept: image/webp. IMGPROXY_MAX_SRC_RESOLUTION: '50' IMGPROXY_ENABLE_WEBP_DETECTION: 'true' # Bind on 8080 (default) — Traefik reaches it over dokploy-network only. IMGPROXY_BIND: ':8080' healthcheck: # imgproxy ships an /health endpoint that never 500s when the process # is up. imgproxy has no shell/curl, so use its built-in health probe. test: ['CMD', 'imgproxy', 'health'] interval: 15s timeout: 5s retries: 5 start_period: 20s networks: - dokploy-network labels: - 'briven_logs=true' - 'traefik.enable=true' - 'traefik.docker.network=dokploy-network' - 'traefik.http.services.briven-imgproxy.loadbalancer.server.port=8080' # Higher priority than briven-media so /_t/* hits imgproxy; rest of # media. stays on the api. IMGPROXY_PATH_PREFIX=/_t strips prefix. - 'traefik.http.routers.briven-imgt.rule=Host(`media.${BRIVEN_DOMAIN}`) && PathPrefix(`/_t`)' - 'traefik.http.routers.briven-imgt.priority=100' - 'traefik.http.routers.briven-imgt.entrypoints=websecure' - 'traefik.http.routers.briven-imgt.tls.certresolver=letsencrypt' - 'traefik.http.routers.briven-imgt.service=briven-imgproxy' runtime: build: context: ../.. dockerfile: apps/runtime/Dockerfile restart: unless-stopped logging: *briven-logging depends_on: api: condition: service_started doltgres: condition: service_healthy environment: BRIVEN_ENV: production BRIVEN_RUNTIME_PORT: '3003' BRIVEN_RUNTIME_SHARED_SECRET: ${BRIVEN_RUNTIME_SHARED_SECRET} BRIVEN_RUNTIME_EXECUTOR: deno BRIVEN_RUNTIME_BUNDLE_DIR: /var/lib/briven/bundles BRIVEN_API_INTERNAL_URL: http://api:3001 # Data plane — DoltGres (the old BRIVEN_URL mysql:// was wrong, removed). BRIVEN_DATA_PLANE_URL: postgres://postgres:${BRIVEN_DOLTGRES_PASSWORD}@doltgres:5432/postgres?sslmode=disable volumes: - runtime_bundles:/var/lib/briven/bundles healthcheck: # Any HTTP response = process is up (port serving). test: ['CMD-SHELL', "bun -e \"fetch('http://localhost:3003/').then(()=>process.exit(0)).catch(()=>process.exit(1))\""] interval: 15s timeout: 5s retries: 5 start_period: 40s networks: - briven labels: - 'briven_logs=true' realtime: build: context: ../.. dockerfile: apps/realtime/Dockerfile restart: unless-stopped logging: *briven-logging depends_on: doltgres: condition: service_healthy environment: BRIVEN_ENV: production BRIVEN_REALTIME_PORT: '3004' BRIVEN_API_INTERNAL_URL: http://api:3001 BRIVEN_RUNTIME_SHARED_SECRET: ${BRIVEN_RUNTIME_SHARED_SECRET} # Data plane — DoltGres. Realtime polls DOLT_HASHOF('HEAD') per project # (no LISTEN/NOTIFY on DoltGres). The old BRIVEN_URL mysql:// was wrong. BRIVEN_DATA_PLANE_URL: postgres://postgres:${BRIVEN_DOLTGRES_PASSWORD}@doltgres:5432/postgres?sslmode=disable BRIVEN_REALTIME_POLL_MS: '500' healthcheck: test: ['CMD-SHELL', "bun -e \"fetch('http://localhost:3004/').then(()=>process.exit(0)).catch(()=>process.exit(1))\""] interval: 15s timeout: 5s retries: 5 start_period: 40s networks: - briven - dokploy-network labels: - 'briven_logs=true' - 'traefik.enable=true' - 'traefik.docker.network=dokploy-network' - 'traefik.http.routers.briven-realtime.rule=Host(`realtime.${BRIVEN_DOMAIN}`)' - 'traefik.http.routers.briven-realtime.entrypoints=websecure' - 'traefik.http.routers.briven-realtime.tls.certresolver=letsencrypt' - 'traefik.http.services.briven-realtime.loadbalancer.server.port=3004' web: build: context: ../.. dockerfile: apps/web/Dockerfile restart: unless-stopped logging: *briven-logging depends_on: api: condition: service_started environment: BRIVEN_API_ORIGIN: https://api.${BRIVEN_DOMAIN} BRIVEN_WEB_ORIGIN: https://${BRIVEN_DOMAIN} NEXT_PUBLIC_BRIVEN_API_ORIGIN: https://api.${BRIVEN_DOMAIN} NEXT_PUBLIC_BRIVEN_HAS_GOOGLE_OAUTH: ${BRIVEN_GOOGLE_CLIENT_ID:+true} NEXT_PUBLIC_BRIVEN_HAS_GITHUB_OAUTH: ${BRIVEN_GITHUB_CLIENT_ID:+true} NEXT_PUBLIC_BRIVEN_HAS_KONNOS_OAUTH: ${BRIVEN_KONNOS_CLIENT_ID:+true} NEXT_PUBLIC_BRIVEN_HAS_DISCORD_OAUTH: ${BRIVEN_DISCORD_CLIENT_ID:+true} healthcheck: # web runs `next start` on node — use node's built-in fetch. test: ['CMD-SHELL', "node -e \"fetch('http://localhost:3000/').then(()=>process.exit(0)).catch(()=>process.exit(1))\""] interval: 15s timeout: 5s retries: 5 start_period: 40s networks: - briven - dokploy-network labels: - 'briven_logs=true' - 'traefik.enable=true' - 'traefik.docker.network=dokploy-network' - 'traefik.http.routers.briven-web.rule=Host(`${BRIVEN_DOMAIN}`) || Host(`app.${BRIVEN_DOMAIN}`) || Host(`admin.${BRIVEN_DOMAIN}`)' - 'traefik.http.routers.briven-web.entrypoints=websecure' - 'traefik.http.routers.briven-web.tls.certresolver=letsencrypt' - 'traefik.http.routers.briven-web.service=briven-web' - 'traefik.http.services.briven-web.loadbalancer.server.port=3000' docs: build: context: ../.. dockerfile: apps/docs/Dockerfile restart: unless-stopped logging: *briven-logging depends_on: api: condition: service_started environment: # Used by /status + /api/status/incidents.xml to read live incidents. BRIVEN_API_ORIGIN: https://api.${BRIVEN_DOMAIN} healthcheck: test: ['CMD-SHELL', "node -e \"fetch('http://localhost:3002/').then(()=>process.exit(0)).catch(()=>process.exit(1))\""] interval: 15s timeout: 5s retries: 5 start_period: 40s networks: - briven - dokploy-network labels: - 'briven_logs=true' - 'traefik.enable=true' - 'traefik.docker.network=dokploy-network' - 'traefik.http.routers.briven-docs.rule=Host(`docs.${BRIVEN_DOMAIN}`)' - 'traefik.http.routers.briven-docs.entrypoints=websecure' - 'traefik.http.routers.briven-docs.tls.certresolver=letsencrypt' - 'traefik.http.routers.briven-docs.service=briven-docs' - 'traefik.http.services.briven-docs.loadbalancer.server.port=3002' # status.${BRIVEN_DOMAIN} — same docs container; bare `/` rewrites to # /status. Other paths pass through (so /api/status/incidents.xml works). - 'traefik.http.routers.briven-status.rule=Host(`status.${BRIVEN_DOMAIN}`)' - 'traefik.http.routers.briven-status.entrypoints=websecure' - 'traefik.http.routers.briven-status.tls.certresolver=letsencrypt' - 'traefik.http.routers.briven-status.service=briven-docs' - 'traefik.http.routers.briven-status.middlewares=briven-status-rewrite' - 'traefik.http.middlewares.briven-status-rewrite.replacepathregex.regex=^/$$' - 'traefik.http.middlewares.briven-status-rewrite.replacepathregex.replacement=/status' volumes: postgres_data: doltgres_data: doltgres_backups: redis_data: minio_data: runtime_bundles: networks: briven: driver: bridge # Dokploy's ingress network — Traefik watches this for routing + TLS. # Routed services (api, realtime, web, docs, s3) attach to it in addition # to the internal `briven` network; DBs/redis/runtime stay internal-only. dokploy-network: external: true