compose.dokploy.yml593 lines · main
| 1 | # briven — Dokploy-managed compose (generated from compose.yml). |
| 2 | # Dokploy controls the project name + container names, so this file omits |
| 3 | # the top-level `name:` and per-service `container_name:` that compose.yml |
| 4 | # pins for the raw self-host path. Keep this in sync with compose.yml. |
| 5 | # Source of truth for the engine/services is compose.yml (DoltGres data plane). |
| 6 | |
| 7 | # briven — canonical self-host compose (build-from-source). |
| 8 | # |
| 9 | # Product line (flndrn 2026-07-21): Briven is Doltgres end-to-end. |
| 10 | # |
| 11 | # - control plane = DoltGres database `briven_control` on the doltgres service |
| 12 | # (sign-in, orgs, projects, billing, Briven Auth). BRIVEN_DATABASE_URL. |
| 13 | # - data plane = same DoltGres cluster, one DATABASE per customer project |
| 14 | # (`proj_…`). BRIVEN_DATA_PLANE_URL. Realtime polls DOLT_HASHOF('HEAD'). |
| 15 | # |
| 16 | # Stock Postgres (pgvector) may still exist on the host for rollback only — |
| 17 | # live traffic must not use it. Do not reintroduce control-on-Postgres. |
| 18 | # |
| 19 | # This file supersedes the two earlier conflicting composes: |
| 20 | # - the old Dolt-MySQL build (dolthub/dolt-sql-server + BRIVEN_URL mysql) — |
| 21 | # wrong engine, broke the api. REMOVED. |
| 22 | # - the all-pgvector build (data plane as a 2nd Postgres DB) — wrong data |
| 23 | # plane. REPLACED by the real DoltGres service below. |
| 24 | # |
| 25 | # Dokploy clones the repo and runs `docker compose build` against the local |
| 26 | # Dockerfiles — no external registry, no GHCR, no docker.sock. Per |
| 27 | # docs/DOCKER.md §7 / infra/CLAUDE.md: every long-running service caps its |
| 28 | # log volume via the *briven-logging anchor; no watchtower, no docker_sd, |
| 29 | # no registry polling on the host. |
| 30 | # |
| 31 | # Single-machine layout, ~25 concurrent customer projects. Past that, split |
| 32 | # the control plane onto one host and the data plane (doltgres + minio) onto |
| 33 | # another. |
| 34 | # |
| 35 | # Required env (drop a `.env` next to this file — see .env.example): |
| 36 | # |
| 37 | # BRIVEN_DOMAIN e.g. briven.example.com |
| 38 | # BRIVEN_BETTER_AUTH_SECRET openssl rand -hex 32 |
| 39 | # BRIVEN_AUDIT_IP_PEPPER openssl rand -hex 32 |
| 40 | # BRIVEN_ENCRYPTION_KEY openssl rand -hex 32 |
| 41 | # BRIVEN_RUNTIME_SHARED_SECRET openssl rand -hex 32 |
| 42 | # BRIVEN_POSTGRES_PASSWORD control-plane Postgres superuser password |
| 43 | # BRIVEN_DOLTGRES_PASSWORD data-plane DoltGres superuser password |
| 44 | # BRIVEN_MINIO_ROOT_PASSWORD MinIO root / S3 secret key |
| 45 | # (optional) BRIVEN_MITTERA_*, BRIVEN_*_CLIENT_ID/SECRET, BRIVEN_POLAR_*, |
| 46 | # BRIVEN_OLLAMA_*, BRIVEN_MINIO_BUCKET/REGION, BRIVEN_OPEN_SIGNUPS |
| 47 | # |
| 48 | # After first boot: |
| 49 | # 1. Create the first user via the magic-link flow on https://${BRIVEN_DOMAIN} |
| 50 | # 2. Promote to admin in the control plane: |
| 51 | # docker exec -it briven-postgres psql -U postgres -d briven_control \ |
| 52 | # -c "UPDATE users SET is_admin = true WHERE id = '...';" |
| 53 | # 3. Create your first project via the dashboard (provisions a DoltGres DB). |
| 54 | |
| 55 | x-logging: &briven-logging |
| 56 | driver: json-file |
| 57 | options: |
| 58 | max-size: '10m' |
| 59 | max-file: '3' |
| 60 | |
| 61 | services: |
| 62 | # ─── control plane ──────────────────────────────────────────────────── |
| 63 | postgres: |
| 64 | image: pgvector/pgvector:pg17 |
| 65 | restart: unless-stopped |
| 66 | logging: *briven-logging |
| 67 | environment: |
| 68 | POSTGRES_PASSWORD: ${BRIVEN_POSTGRES_PASSWORD} |
| 69 | POSTGRES_DB: briven_control |
| 70 | volumes: |
| 71 | - postgres_data:/var/lib/postgresql/data |
| 72 | # Control-plane init only: enables pgvector + pg_trgm on briven_control. |
| 73 | # No data-plane DB is created here — the data plane is the doltgres |
| 74 | # service, with a database per project (see ADR-0002). |
| 75 | - ./postgres-init:/docker-entrypoint-initdb.d:ro |
| 76 | healthcheck: |
| 77 | test: ['CMD-SHELL', 'pg_isready -U postgres -d briven_control'] |
| 78 | interval: 10s |
| 79 | timeout: 5s |
| 80 | retries: 5 |
| 81 | start_period: 20s |
| 82 | networks: |
| 83 | - briven |
| 84 | labels: |
| 85 | - 'briven_logs=true' |
| 86 | |
| 87 | # ─── data plane ─────────────────────────────────────────────────────── |
| 88 | # DoltGres = Postgres-wire, git-for-data. Each customer project is its own |
| 89 | # DATABASE here, created by the api over the `pg` driver. The default |
| 90 | # superuser/database is `postgres`/`postgres` (DOLTGRES_* envs override the |
| 91 | # password). |
| 92 | # |
| 93 | # IMAGE IS PINNED BY DIGEST, ON PURPOSE (2026-07-07 maintenance window): |
| 94 | # `:latest` let a deploy silently swap the database engine under live data |
| 95 | # (prime suspect in the 2026-07-07 auth.db outage). To upgrade the engine, |
| 96 | # change the digest here deliberately, in its own reviewed deploy. |
| 97 | # |
| 98 | # DATA DIR IS /var/lib/doltgres — NO "ql". The mount below once pointed at |
| 99 | # /var/lib/doltgresql (typo), so all real data lived in an anonymous volume |
| 100 | # that a container recreation would orphan. Fixed 2026-07-07 (data migrated |
| 101 | # into the named volume during the maintenance window). Never change this |
| 102 | # path without checking `config.yaml` inside the volume. |
| 103 | doltgres: |
| 104 | image: dolthub/doltgresql:0.57.2 |
| 105 | restart: unless-stopped |
| 106 | logging: *briven-logging |
| 107 | environment: |
| 108 | DOLTGRES_USER: postgres |
| 109 | DOLTGRES_PASSWORD: ${BRIVEN_DOLTGRES_PASSWORD} |
| 110 | volumes: |
| 111 | - doltgres_data:/var/lib/doltgres |
| 112 | # Dolt-native backups land here (written by the server itself); the host |
| 113 | # systemd job infra/backups/briven-backup.sh triggers them (gentle, one |
| 114 | # DB at a time). The old in-compose dolt-backup sidecar was REMOVED |
| 115 | # 2026-08-01 — its un-throttled per-DB loop helped lock DoltGres under load. |
| 116 | - doltgres_backups:/backups |
| 117 | healthcheck: |
| 118 | # pg_isready ships in the doltgresql image and needs no password. |
| 119 | test: ['CMD-SHELL', 'pg_isready -h 127.0.0.1 -p 5432 -U postgres'] |
| 120 | interval: 10s |
| 121 | timeout: 5s |
| 122 | retries: 5 |
| 123 | start_period: 30s |
| 124 | networks: |
| 125 | - briven |
| 126 | labels: |
| 127 | - 'briven_logs=true' |
| 128 | |
| 129 | # ─── data-plane backup: moved to the host systemd job (2026-08-01) ────── |
| 130 | # The in-compose `dolt-backup` sidecar was REMOVED. Its un-throttled loop of |
| 131 | # `dolt_backup('sync-url', …)` over every DB helped lock DoltGres 0.56.6 under |
| 132 | # load (full-platform outage, see docs/knowledge-base.md → INCIDENT 2026-08-01). |
| 133 | # Backups now run from infra/backups/briven-backup.sh (systemd timer): a GENTLE, |
| 134 | # one-DB-at-a-time, throttled `dolt_backup` pass on the fixed 0.57.2 engine, |
| 135 | # plus off-site mirror. Restorable via `dolt backup restore`. |
| 136 | # |
| 137 | # NOTE (still true): `pg_dump` CANNOT back up the DoltGres data plane — it opens |
| 138 | # a REPEATABLE READ READ ONLY snapshot txn DoltGres doesn't implement |
| 139 | # ("SET TRANSACTION is not yet supported"). Dolt-native `dolt_backup` is the |
| 140 | # only data-plane backup path; pg_dump is a stock-Postgres control-plane helper. |
| 141 | |
| 142 | # ─── briven-engine Auth DB (DOLTGRES ONLY — non-negotiable) ─────────── |
| 143 | # HARD RULE: the COMPLETE Briven project is Doltgres. New parts do not get |
| 144 | # a separate stock-Postgres brain. SuperTokens Core Docker is REMOVED — |
| 145 | # it is incompatible with Doltgres (SET SESSION CHARACTERISTICS). |
| 146 | # briven-engine = Briven API code + tables in Doltgres DB `briven_engine`. |
| 147 | # Create/migrate: API `ensureBrivenEngineDatabase` + schema bootstrap. |
| 148 | briven-engine-db-init: |
| 149 | image: dolthub/doltgresql:0.57.2 |
| 150 | restart: 'no' |
| 151 | logging: *briven-logging |
| 152 | depends_on: |
| 153 | doltgres: |
| 154 | condition: service_healthy |
| 155 | environment: |
| 156 | PGPASSWORD: ${BRIVEN_DOLTGRES_PASSWORD} |
| 157 | entrypoint: |
| 158 | - /bin/sh |
| 159 | - -c |
| 160 | - | |
| 161 | set -e |
| 162 | echo "briven-engine-db-init: ensuring briven_engine on DOLTGRES only…" |
| 163 | exists=$$(psql -h doltgres -U postgres -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname='briven_engine'" || true) |
| 164 | if [ "$$exists" = "1" ]; then |
| 165 | echo "briven_engine already exists on doltgres" |
| 166 | else |
| 167 | psql -h doltgres -U postgres -d postgres -c 'CREATE DATABASE briven_engine' |
| 168 | echo "created briven_engine on doltgres" |
| 169 | fi |
| 170 | networks: |
| 171 | - briven |
| 172 | labels: |
| 173 | - 'briven.service=briven-engine-db-init' |
| 174 | - 'briven.db=doltgres' |
| 175 | |
| 176 | # ─── shared infra ───────────────────────────────────────────────────── |
| 177 | redis: |
| 178 | image: redis:7.4-alpine |
| 179 | restart: unless-stopped |
| 180 | logging: *briven-logging |
| 181 | command: redis-server --appendonly yes |
| 182 | volumes: |
| 183 | - redis_data:/data |
| 184 | healthcheck: |
| 185 | test: ['CMD-SHELL', 'redis-cli ping | grep -q PONG'] |
| 186 | interval: 10s |
| 187 | timeout: 5s |
| 188 | retries: 5 |
| 189 | start_period: 10s |
| 190 | networks: |
| 191 | - briven |
| 192 | labels: |
| 193 | - 'briven_logs=true' |
| 194 | |
| 195 | minio: |
| 196 | image: minio/minio:latest |
| 197 | restart: unless-stopped |
| 198 | logging: *briven-logging |
| 199 | command: server /data --console-address ':9001' |
| 200 | environment: |
| 201 | MINIO_ROOT_USER: briven |
| 202 | MINIO_ROOT_PASSWORD: ${BRIVEN_MINIO_ROOT_PASSWORD} |
| 203 | volumes: |
| 204 | - minio_data:/data |
| 205 | healthcheck: |
| 206 | test: ['CMD-SHELL', 'curl -fsS http://localhost:9000/minio/health/live || exit 1'] |
| 207 | interval: 15s |
| 208 | timeout: 5s |
| 209 | retries: 5 |
| 210 | start_period: 20s |
| 211 | networks: |
| 212 | - briven |
| 213 | - dokploy-network |
| 214 | labels: |
| 215 | - 'briven_logs=true' |
| 216 | - 'traefik.enable=true' |
| 217 | - 'traefik.docker.network=dokploy-network' |
| 218 | # Public S3 endpoint — browsers PUT/GET with sigv4-presigned URLs the |
| 219 | # api mints. The api also reaches MinIO internally at http://minio:9000. |
| 220 | - 'traefik.http.routers.briven-s3.rule=Host(`s3.${BRIVEN_DOMAIN}`)' |
| 221 | - 'traefik.http.routers.briven-s3.entrypoints=websecure' |
| 222 | - 'traefik.http.routers.briven-s3.tls.certresolver=letsencrypt' |
| 223 | - 'traefik.http.routers.briven-s3.service=briven-s3' |
| 224 | - 'traefik.http.services.briven-s3.loadbalancer.server.port=9000' |
| 225 | |
| 226 | # One-shot bucket creator. `mc mb --ignore-existing` is idempotent, so this |
| 227 | # runs every deploy and no-ops after the first. restart: 'no' = one-shot, so |
| 228 | # per infra/CLAUDE.md it does NOT need the logging cap. |
| 229 | minio-init: |
| 230 | image: minio/mc:latest |
| 231 | depends_on: |
| 232 | minio: |
| 233 | condition: service_healthy |
| 234 | entrypoint: > |
| 235 | /bin/sh -c " |
| 236 | until /usr/bin/mc alias set minio http://minio:9000 briven ${BRIVEN_MINIO_ROOT_PASSWORD} >/dev/null 2>&1; do |
| 237 | echo 'waiting for minio...'; sleep 2; |
| 238 | done; |
| 239 | /usr/bin/mc mb --ignore-existing minio/${BRIVEN_MINIO_BUCKET:-briven}; |
| 240 | echo 'minio bucket ready: ${BRIVEN_MINIO_BUCKET:-briven}'; |
| 241 | " |
| 242 | restart: 'no' |
| 243 | networks: |
| 244 | - briven |
| 245 | |
| 246 | # ─── application services ───────────────────────────────────────────── |
| 247 | api: |
| 248 | build: |
| 249 | context: ../.. |
| 250 | dockerfile: apps/api/Dockerfile |
| 251 | restart: unless-stopped |
| 252 | logging: *briven-logging |
| 253 | depends_on: |
| 254 | postgres: |
| 255 | condition: service_healthy |
| 256 | doltgres: |
| 257 | condition: service_healthy |
| 258 | redis: |
| 259 | condition: service_healthy |
| 260 | briven-engine-db-init: |
| 261 | condition: service_completed_successfully |
| 262 | environment: |
| 263 | BRIVEN_ENV: production |
| 264 | BRIVEN_API_PORT: '3001' |
| 265 | BRIVEN_API_ORIGIN: https://api.${BRIVEN_DOMAIN} |
| 266 | BRIVEN_WEB_ORIGIN: https://${BRIVEN_DOMAIN} |
| 267 | BRIVEN_ADMIN_ORIGIN: https://admin.${BRIVEN_DOMAIN} |
| 268 | # briven-engine = API + Doltgres only (NO SuperTokens Core container). |
| 269 | BRIVEN_AUTH_CORE_ENABLED: ${BRIVEN_AUTH_CORE_ENABLED:-true} |
| 270 | # SQL for Auth vault — ALWAYS Doltgres DB briven_engine (DOLTGRES-FIRST). |
| 271 | BRIVEN_ENGINE_DATABASE_URL: postgres://postgres:${BRIVEN_DOLTGRES_PASSWORD}@doltgres:5432/briven_engine?sslmode=disable |
| 272 | BRIVEN_AUTH_ENABLED: ${BRIVEN_AUTH_ENABLED:-false} |
| 273 | # Observability stack (host-managed compose at /root/briven-observability |
| 274 | # on the France box, same dokploy-network) — powers admin host gauges, |
| 275 | # the live cpu chart, and /v1/admin/timeseries prom-backed series. |
| 276 | BRIVEN_PROMETHEUS_URL: ${BRIVEN_PROMETHEUS_URL:-} |
| 277 | # Hard allowlist: only these emails can EVER be platform admin |
| 278 | # (see apps/api/src/lib/superadmin.ts). Value lives in the Dokploy env. |
| 279 | BRIVEN_SUPERADMIN_EMAILS: ${BRIVEN_SUPERADMIN_EMAILS:-} |
| 280 | BRIVEN_TRUSTED_ORIGINS: https://${BRIVEN_DOMAIN},https://app.${BRIVEN_DOMAIN},https://api.${BRIVEN_DOMAIN},https://admin.${BRIVEN_DOMAIN} |
| 281 | # Control plane — stock Postgres, postgres.js/drizzle. |
| 282 | # Control brain on Doltgres (same cluster as project DBs). Password is |
| 283 | # DOLTGRES, not the legacy stock-Postgres password. |
| 284 | BRIVEN_DATABASE_URL: postgres://postgres:${BRIVEN_DOLTGRES_PASSWORD}@doltgres:5432/briven_control?sslmode=disable |
| 285 | # Data plane — DoltGres, `pg` driver, database-per-project. The api |
| 286 | # connects to the default `postgres` database and CREATEs per-project |
| 287 | # databases on this server. |
| 288 | BRIVEN_DATA_PLANE_URL: postgres://postgres:${BRIVEN_DOLTGRES_PASSWORD}@doltgres:5432/postgres?sslmode=disable |
| 289 | BRIVEN_REDIS_URL: redis://redis:6379 |
| 290 | BRIVEN_RUNTIME_URL: http://runtime:3003 |
| 291 | # Without this the api falls back to localhost:3004 and can't reach |
| 292 | # realtime — surfaces as realtime_stats_failed "Unable to connect". |
| 293 | BRIVEN_REALTIME_URL: http://realtime:3004 |
| 294 | BRIVEN_RUNTIME_SHARED_SECRET: ${BRIVEN_RUNTIME_SHARED_SECRET} |
| 295 | BRIVEN_BETTER_AUTH_SECRET: ${BRIVEN_BETTER_AUTH_SECRET} |
| 296 | BRIVEN_AUDIT_IP_PEPPER: ${BRIVEN_AUDIT_IP_PEPPER} |
| 297 | BRIVEN_ENCRYPTION_KEY: ${BRIVEN_ENCRYPTION_KEY} |
| 298 | # Per-tenant secret-store master key (ARCHITECTURE.md §4/§9). |
| 299 | # BRIVEN_AUTH_ENABLED is set once above with the engine env block. |
| 300 | # Values live in the Dokploy env panel; empty = unset in loadEnv. |
| 301 | BRIVEN_AUTH_MASTER_KEY: ${BRIVEN_AUTH_MASTER_KEY:-} |
| 302 | BRIVEN_MITTERA_API_URL: ${BRIVEN_MITTERA_API_URL:-} |
| 303 | BRIVEN_MITTERA_API_KEY: ${BRIVEN_MITTERA_API_KEY:-} |
| 304 | BRIVEN_MITTERA_WEBHOOK_SECRET: ${BRIVEN_MITTERA_WEBHOOK_SECRET:-} |
| 305 | # Auth OTP / magic-link real inbox: set HOST+USER+PASS(+FROM) for SMTP primary. |
| 306 | # Until SMTP is set, Auth uses mittera (same as platform mail). |
| 307 | BRIVEN_SMTP_HOST: ${BRIVEN_SMTP_HOST:-} |
| 308 | BRIVEN_SMTP_PORT: ${BRIVEN_SMTP_PORT:-587} |
| 309 | BRIVEN_SMTP_USER: ${BRIVEN_SMTP_USER:-} |
| 310 | BRIVEN_SMTP_PASS: ${BRIVEN_SMTP_PASS:-} |
| 311 | BRIVEN_SMTP_FROM: ${BRIVEN_SMTP_FROM:-} |
| 312 | BRIVEN_GOOGLE_CLIENT_ID: ${BRIVEN_GOOGLE_CLIENT_ID:-} |
| 313 | BRIVEN_GOOGLE_CLIENT_SECRET: ${BRIVEN_GOOGLE_CLIENT_SECRET:-} |
| 314 | BRIVEN_GITHUB_CLIENT_ID: ${BRIVEN_GITHUB_CLIENT_ID:-} |
| 315 | BRIVEN_GITHUB_CLIENT_SECRET: ${BRIVEN_GITHUB_CLIENT_SECRET:-} |
| 316 | BRIVEN_KONNOS_CLIENT_ID: ${BRIVEN_KONNOS_CLIENT_ID:-} |
| 317 | BRIVEN_KONNOS_CLIENT_SECRET: ${BRIVEN_KONNOS_CLIENT_SECRET:-} |
| 318 | BRIVEN_KONNOS_ISSUER: ${BRIVEN_KONNOS_ISSUER:-https://code.konnos.org} |
| 319 | BRIVEN_DISCORD_CLIENT_ID: ${BRIVEN_DISCORD_CLIENT_ID:-} |
| 320 | BRIVEN_DISCORD_CLIENT_SECRET: ${BRIVEN_DISCORD_CLIENT_SECRET:-} |
| 321 | BRIVEN_POLAR_API_BASE: ${BRIVEN_POLAR_API_BASE:-https://api.polar.sh} |
| 322 | BRIVEN_POLAR_ACCESS_TOKEN: ${BRIVEN_POLAR_ACCESS_TOKEN:-} |
| 323 | BRIVEN_POLAR_WEBHOOK_SECRET: ${BRIVEN_POLAR_WEBHOOK_SECRET:-} |
| 324 | BRIVEN_POLAR_PRO_PRODUCT_ID: ${BRIVEN_POLAR_PRO_PRODUCT_ID:-} |
| 325 | BRIVEN_POLAR_TEAM_PRODUCT_ID: ${BRIVEN_POLAR_TEAM_PRODUCT_ID:-} |
| 326 | BRIVEN_DOMAIN: ${BRIVEN_DOMAIN} |
| 327 | BRIVEN_OPEN_SIGNUPS: ${BRIVEN_OPEN_SIGNUPS:-false} |
| 328 | BRIVEN_OLLAMA_URL: ${BRIVEN_OLLAMA_URL:-} |
| 329 | BRIVEN_OLLAMA_API_KEY: ${BRIVEN_OLLAMA_API_KEY:-} |
| 330 | BRIVEN_OLLAMA_MODEL: ${BRIVEN_OLLAMA_MODEL:-qwen2.5-coder:32b} |
| 331 | BRIVEN_MINIO_ENDPOINT: http://minio:9000 |
| 332 | BRIVEN_MINIO_PUBLIC_ENDPOINT: https://s3.${BRIVEN_DOMAIN} |
| 333 | BRIVEN_MINIO_ACCESS_KEY: briven |
| 334 | BRIVEN_MINIO_SECRET_KEY: ${BRIVEN_MINIO_ROOT_PASSWORD} |
| 335 | BRIVEN_MINIO_BUCKET: ${BRIVEN_MINIO_BUCKET:-briven} |
| 336 | BRIVEN_MINIO_REGION: ${BRIVEN_MINIO_REGION:-us-east-1} |
| 337 | # imgproxy — on-the-fly image transforms (M4). The api mints SIGNED |
| 338 | # imgproxy URLs (services/image-transform.ts). ENDPOINT is the |
| 339 | # media host + the /_t path prefix (imgproxy sits BEHIND media.<domain> |
| 340 | # under PathPrefix(/_t), no new subdomain). image-transform.ts signs the |
| 341 | # path WITHOUT the prefix (/rs:.../g:sm/<b64 source>) and imgproxy strips |
| 342 | # /_t before verifying — the signed portions match. KEY/SALT are the same |
| 343 | # hex secrets the imgproxy container gets; set all three in the Dokploy |
| 344 | # env to turn transforms on (unset => endpoint returns 503, fail-safe). |
| 345 | BRIVEN_IMGPROXY_ENDPOINT: https://media.${BRIVEN_DOMAIN}/_t |
| 346 | BRIVEN_IMGPROXY_KEY: ${BRIVEN_IMGPROXY_KEY:-} |
| 347 | BRIVEN_IMGPROXY_SALT: ${BRIVEN_IMGPROXY_SALT:-} |
| 348 | # Self-hosted MaxMind GeoLite2-City for Auth email Location (city/country). |
| 349 | # Host file: /var/lib/GeoIP/GeoLite2-City.mmdb (bind-mounted below). |
| 350 | BRIVEN_GEOIP_DB_PATH: ${BRIVEN_GEOIP_DB_PATH:-/var/lib/GeoIP/GeoLite2-City.mmdb} |
| 351 | volumes: |
| 352 | # Read-only city DB for Auth email geo (device location line). |
| 353 | - /var/lib/GeoIP:/var/lib/GeoIP:ro |
| 354 | healthcheck: |
| 355 | # /info is documented to never 500 (apps/api/Dockerfile). bun ships in |
| 356 | # the api image and has a built-in fetch. |
| 357 | test: ['CMD-SHELL', "bun -e \"fetch('http://localhost:3001/info').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""] |
| 358 | interval: 15s |
| 359 | timeout: 5s |
| 360 | retries: 5 |
| 361 | start_period: 40s |
| 362 | networks: |
| 363 | - briven |
| 364 | - dokploy-network |
| 365 | labels: |
| 366 | - 'briven_logs=true' |
| 367 | - 'traefik.enable=true' |
| 368 | - 'traefik.docker.network=dokploy-network' |
| 369 | - 'traefik.http.routers.briven-api.rule=Host(`api.${BRIVEN_DOMAIN}`)' |
| 370 | - 'traefik.http.routers.briven-api.entrypoints=websecure' |
| 371 | - 'traefik.http.routers.briven-api.tls.certresolver=letsencrypt' |
| 372 | - 'traefik.http.routers.briven-api.service=briven-api' |
| 373 | - 'traefik.http.services.briven-api.loadbalancer.server.port=3001' |
| 374 | # media.briven.tech -> this api (public file bytes). Explicit .service= on |
| 375 | # every router so Traefik does not treat the container as multi-service. |
| 376 | - 'traefik.http.routers.briven-media.rule=Host(`media.${BRIVEN_DOMAIN}`)' |
| 377 | - 'traefik.http.routers.briven-media.entrypoints=websecure' |
| 378 | - 'traefik.http.routers.briven-media.tls.certresolver=letsencrypt' |
| 379 | - 'traefik.http.routers.briven-media.service=briven-api' |
| 380 | # /_t image transforms: router lives on the imgproxy service (below), not here. |
| 381 | |
| 382 | # imgproxy — on-the-fly image transforms (M4). Sits BEHIND media.<domain> |
| 383 | # under the /_t path prefix (no new public subdomain, per the media-host |
| 384 | # rule). The api mints signed URLs; imgproxy fetches the source from the |
| 385 | # PUBLIC media host, resizes, and returns it. SSRF is locked to the media |
| 386 | # host via IMGPROXY_ALLOWED_SOURCES. No host port — only Traefik (on the |
| 387 | # dokploy-network) can reach port 8080. |
| 388 | imgproxy: |
| 389 | image: darthsim/imgproxy:latest |
| 390 | restart: unless-stopped |
| 391 | logging: *briven-logging |
| 392 | environment: |
| 393 | # Signing — MUST equal the api's BRIVEN_IMGPROXY_KEY/SALT (same hex |
| 394 | # secrets, set in the Dokploy env). imgproxy strips IMGPROXY_PATH_PREFIX |
| 395 | # (/_t) from the request path, then verifies the HMAC over the remaining |
| 396 | # /rs:.../g:sm/<b64 source> — exactly what image-transform.ts signed. |
| 397 | IMGPROXY_KEY: ${BRIVEN_IMGPROXY_KEY:-} |
| 398 | IMGPROXY_SALT: ${BRIVEN_IMGPROXY_SALT:-} |
| 399 | IMGPROXY_PATH_PREFIX: /_t |
| 400 | # SSRF guard: imgproxy may ONLY fetch sources from the public media host. |
| 401 | IMGPROXY_ALLOWED_SOURCES: https://media.${BRIVEN_DOMAIN}/ |
| 402 | # Sane limits — resize-bomb / oversized-source guards. Format auto-nego: |
| 403 | # serve WebP to browsers that send Accept: image/webp. |
| 404 | IMGPROXY_MAX_SRC_RESOLUTION: '50' |
| 405 | IMGPROXY_ENABLE_WEBP_DETECTION: 'true' |
| 406 | # Bind on 8080 (default) — Traefik reaches it over dokploy-network only. |
| 407 | IMGPROXY_BIND: ':8080' |
| 408 | healthcheck: |
| 409 | # imgproxy ships an /health endpoint that never 500s when the process |
| 410 | # is up. imgproxy has no shell/curl, so use its built-in health probe. |
| 411 | test: ['CMD', 'imgproxy', 'health'] |
| 412 | interval: 15s |
| 413 | timeout: 5s |
| 414 | retries: 5 |
| 415 | start_period: 20s |
| 416 | networks: |
| 417 | - dokploy-network |
| 418 | labels: |
| 419 | - 'briven_logs=true' |
| 420 | - 'traefik.enable=true' |
| 421 | - 'traefik.docker.network=dokploy-network' |
| 422 | - 'traefik.http.services.briven-imgproxy.loadbalancer.server.port=8080' |
| 423 | # Higher priority than briven-media so /_t/* hits imgproxy; rest of |
| 424 | # media.<domain> stays on the api. IMGPROXY_PATH_PREFIX=/_t strips prefix. |
| 425 | - 'traefik.http.routers.briven-imgt.rule=Host(`media.${BRIVEN_DOMAIN}`) && PathPrefix(`/_t`)' |
| 426 | - 'traefik.http.routers.briven-imgt.priority=100' |
| 427 | - 'traefik.http.routers.briven-imgt.entrypoints=websecure' |
| 428 | - 'traefik.http.routers.briven-imgt.tls.certresolver=letsencrypt' |
| 429 | - 'traefik.http.routers.briven-imgt.service=briven-imgproxy' |
| 430 | |
| 431 | runtime: |
| 432 | build: |
| 433 | context: ../.. |
| 434 | dockerfile: apps/runtime/Dockerfile |
| 435 | restart: unless-stopped |
| 436 | logging: *briven-logging |
| 437 | depends_on: |
| 438 | api: |
| 439 | condition: service_started |
| 440 | doltgres: |
| 441 | condition: service_healthy |
| 442 | environment: |
| 443 | BRIVEN_ENV: production |
| 444 | BRIVEN_RUNTIME_PORT: '3003' |
| 445 | BRIVEN_RUNTIME_SHARED_SECRET: ${BRIVEN_RUNTIME_SHARED_SECRET} |
| 446 | BRIVEN_RUNTIME_EXECUTOR: deno |
| 447 | BRIVEN_RUNTIME_BUNDLE_DIR: /var/lib/briven/bundles |
| 448 | BRIVEN_API_INTERNAL_URL: http://api:3001 |
| 449 | # Data plane — DoltGres (the old BRIVEN_URL mysql:// was wrong, removed). |
| 450 | BRIVEN_DATA_PLANE_URL: postgres://postgres:${BRIVEN_DOLTGRES_PASSWORD}@doltgres:5432/postgres?sslmode=disable |
| 451 | volumes: |
| 452 | - runtime_bundles:/var/lib/briven/bundles |
| 453 | healthcheck: |
| 454 | # Any HTTP response = process is up (port serving). |
| 455 | test: ['CMD-SHELL', "bun -e \"fetch('http://localhost:3003/').then(()=>process.exit(0)).catch(()=>process.exit(1))\""] |
| 456 | interval: 15s |
| 457 | timeout: 5s |
| 458 | retries: 5 |
| 459 | start_period: 40s |
| 460 | networks: |
| 461 | - briven |
| 462 | labels: |
| 463 | - 'briven_logs=true' |
| 464 | |
| 465 | realtime: |
| 466 | build: |
| 467 | context: ../.. |
| 468 | dockerfile: apps/realtime/Dockerfile |
| 469 | restart: unless-stopped |
| 470 | logging: *briven-logging |
| 471 | depends_on: |
| 472 | doltgres: |
| 473 | condition: service_healthy |
| 474 | environment: |
| 475 | BRIVEN_ENV: production |
| 476 | BRIVEN_REALTIME_PORT: '3004' |
| 477 | BRIVEN_API_INTERNAL_URL: http://api:3001 |
| 478 | BRIVEN_RUNTIME_SHARED_SECRET: ${BRIVEN_RUNTIME_SHARED_SECRET} |
| 479 | # Data plane — DoltGres. Realtime polls DOLT_HASHOF('HEAD') per project |
| 480 | # (no LISTEN/NOTIFY on DoltGres). The old BRIVEN_URL mysql:// was wrong. |
| 481 | BRIVEN_DATA_PLANE_URL: postgres://postgres:${BRIVEN_DOLTGRES_PASSWORD}@doltgres:5432/postgres?sslmode=disable |
| 482 | BRIVEN_REALTIME_POLL_MS: '500' |
| 483 | healthcheck: |
| 484 | test: ['CMD-SHELL', "bun -e \"fetch('http://localhost:3004/').then(()=>process.exit(0)).catch(()=>process.exit(1))\""] |
| 485 | interval: 15s |
| 486 | timeout: 5s |
| 487 | retries: 5 |
| 488 | start_period: 40s |
| 489 | networks: |
| 490 | - briven |
| 491 | - dokploy-network |
| 492 | labels: |
| 493 | - 'briven_logs=true' |
| 494 | - 'traefik.enable=true' |
| 495 | - 'traefik.docker.network=dokploy-network' |
| 496 | - 'traefik.http.routers.briven-realtime.rule=Host(`realtime.${BRIVEN_DOMAIN}`)' |
| 497 | - 'traefik.http.routers.briven-realtime.entrypoints=websecure' |
| 498 | - 'traefik.http.routers.briven-realtime.tls.certresolver=letsencrypt' |
| 499 | - 'traefik.http.services.briven-realtime.loadbalancer.server.port=3004' |
| 500 | |
| 501 | web: |
| 502 | build: |
| 503 | context: ../.. |
| 504 | dockerfile: apps/web/Dockerfile |
| 505 | restart: unless-stopped |
| 506 | logging: *briven-logging |
| 507 | depends_on: |
| 508 | api: |
| 509 | condition: service_started |
| 510 | environment: |
| 511 | BRIVEN_API_ORIGIN: https://api.${BRIVEN_DOMAIN} |
| 512 | BRIVEN_WEB_ORIGIN: https://${BRIVEN_DOMAIN} |
| 513 | NEXT_PUBLIC_BRIVEN_API_ORIGIN: https://api.${BRIVEN_DOMAIN} |
| 514 | NEXT_PUBLIC_BRIVEN_HAS_GOOGLE_OAUTH: ${BRIVEN_GOOGLE_CLIENT_ID:+true} |
| 515 | NEXT_PUBLIC_BRIVEN_HAS_GITHUB_OAUTH: ${BRIVEN_GITHUB_CLIENT_ID:+true} |
| 516 | NEXT_PUBLIC_BRIVEN_HAS_KONNOS_OAUTH: ${BRIVEN_KONNOS_CLIENT_ID:+true} |
| 517 | NEXT_PUBLIC_BRIVEN_HAS_DISCORD_OAUTH: ${BRIVEN_DISCORD_CLIENT_ID:+true} |
| 518 | healthcheck: |
| 519 | # web runs `next start` on node — use node's built-in fetch. |
| 520 | test: ['CMD-SHELL', "node -e \"fetch('http://localhost:3000/').then(()=>process.exit(0)).catch(()=>process.exit(1))\""] |
| 521 | interval: 15s |
| 522 | timeout: 5s |
| 523 | retries: 5 |
| 524 | start_period: 40s |
| 525 | networks: |
| 526 | - briven |
| 527 | - dokploy-network |
| 528 | labels: |
| 529 | - 'briven_logs=true' |
| 530 | - 'traefik.enable=true' |
| 531 | - 'traefik.docker.network=dokploy-network' |
| 532 | - 'traefik.http.routers.briven-web.rule=Host(`${BRIVEN_DOMAIN}`) || Host(`app.${BRIVEN_DOMAIN}`) || Host(`admin.${BRIVEN_DOMAIN}`)' |
| 533 | - 'traefik.http.routers.briven-web.entrypoints=websecure' |
| 534 | - 'traefik.http.routers.briven-web.tls.certresolver=letsencrypt' |
| 535 | - 'traefik.http.routers.briven-web.service=briven-web' |
| 536 | - 'traefik.http.services.briven-web.loadbalancer.server.port=3000' |
| 537 | |
| 538 | docs: |
| 539 | build: |
| 540 | context: ../.. |
| 541 | dockerfile: apps/docs/Dockerfile |
| 542 | restart: unless-stopped |
| 543 | logging: *briven-logging |
| 544 | depends_on: |
| 545 | api: |
| 546 | condition: service_started |
| 547 | environment: |
| 548 | # Used by /status + /api/status/incidents.xml to read live incidents. |
| 549 | BRIVEN_API_ORIGIN: https://api.${BRIVEN_DOMAIN} |
| 550 | healthcheck: |
| 551 | test: ['CMD-SHELL', "node -e \"fetch('http://localhost:3002/').then(()=>process.exit(0)).catch(()=>process.exit(1))\""] |
| 552 | interval: 15s |
| 553 | timeout: 5s |
| 554 | retries: 5 |
| 555 | start_period: 40s |
| 556 | networks: |
| 557 | - briven |
| 558 | - dokploy-network |
| 559 | labels: |
| 560 | - 'briven_logs=true' |
| 561 | - 'traefik.enable=true' |
| 562 | - 'traefik.docker.network=dokploy-network' |
| 563 | - 'traefik.http.routers.briven-docs.rule=Host(`docs.${BRIVEN_DOMAIN}`)' |
| 564 | - 'traefik.http.routers.briven-docs.entrypoints=websecure' |
| 565 | - 'traefik.http.routers.briven-docs.tls.certresolver=letsencrypt' |
| 566 | - 'traefik.http.routers.briven-docs.service=briven-docs' |
| 567 | - 'traefik.http.services.briven-docs.loadbalancer.server.port=3002' |
| 568 | # status.${BRIVEN_DOMAIN} — same docs container; bare `/` rewrites to |
| 569 | # /status. Other paths pass through (so /api/status/incidents.xml works). |
| 570 | - 'traefik.http.routers.briven-status.rule=Host(`status.${BRIVEN_DOMAIN}`)' |
| 571 | - 'traefik.http.routers.briven-status.entrypoints=websecure' |
| 572 | - 'traefik.http.routers.briven-status.tls.certresolver=letsencrypt' |
| 573 | - 'traefik.http.routers.briven-status.service=briven-docs' |
| 574 | - 'traefik.http.routers.briven-status.middlewares=briven-status-rewrite' |
| 575 | - 'traefik.http.middlewares.briven-status-rewrite.replacepathregex.regex=^/$$' |
| 576 | - 'traefik.http.middlewares.briven-status-rewrite.replacepathregex.replacement=/status' |
| 577 | |
| 578 | volumes: |
| 579 | postgres_data: |
| 580 | doltgres_data: |
| 581 | doltgres_backups: |
| 582 | redis_data: |
| 583 | minio_data: |
| 584 | runtime_bundles: |
| 585 | |
| 586 | networks: |
| 587 | briven: |
| 588 | driver: bridge |
| 589 | # Dokploy's ingress network — Traefik watches this for routing + TLS. |
| 590 | # Routed services (api, realtime, web, docs, s3) attach to it in addition |
| 591 | # to the internal `briven` network; DBs/redis/runtime stay internal-only. |
| 592 | dokploy-network: |
| 593 | external: true |