compose.dokploy.yml662 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
55x-logging: &briven-logging
56 driver: json-file
57 options:
58 max-size: '10m'
59 max-file: '3'
60
61services:
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@sha256:0483137d0309598d3b0c111dff85d565077bd91cb0524ce00bb832929d5d5ddc
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
113 # dolt-backup service triggers them. See that service for details.
114 - doltgres_backups:/backups
115 healthcheck:
116 # pg_isready ships in the doltgresql image and needs no password.
117 test: ['CMD-SHELL', 'pg_isready -h 127.0.0.1 -p 5432 -U postgres']
118 interval: 10s
119 timeout: 5s
120 retries: 5
121 start_period: 30s
122 networks:
123 - briven
124 labels:
125 - 'briven_logs=true'
126
127 # ─── data-plane backup ──────────────────────────────────────────────────
128 # REAL DoltGres backup (replaces the old placeholder sleep loop).
129 #
130 # Why NOT pg_dump: tested 2026-06-26 against dolthub/doltgresql:latest
131 # (v0.56.6) — `pg_dump` aborts immediately with
132 # "ERROR: SET TRANSACTION is not yet supported"
133 # because pg_dump opens a REPEATABLE READ READ ONLY snapshot transaction
134 # that DoltGres does not implement. So pg_dump CANNOT back up the data
135 # plane. (The control plane is real Postgres and is dumped separately by
136 # the host timers in infra/backups/.)
137 #
138 # What works (verified same day): Dolt's own backup, invoked over the
139 # Postgres wire with `SELECT dolt_backup('sync-url', '<file-url>')`. It
140 # writes a full, version-history-preserving Dolt archive (manifest +
141 # .darc) — re-running it re-syncs in place, so one backup dir per database
142 # already contains every commit (time-travel restore, not just a snapshot).
143 #
144 # This sidecar reuses the doltgresql image (it has `psql`), enumerates the
145 # data-plane databases each run, and asks the doltgres SERVER to back each
146 # one up into the shared `doltgres_backups` volume.
147 #
148 # OFF-SITE follow-up: mirroring the `doltgres_backups` volume to MinIO/B2/R2
149 # is done today by the host systemd timers in infra/backups/ (mc-based, see
150 # briven-backup.sh). Folding an `mc mirror /backups -> minio` step into this
151 # service needs an image carrying both psql and mc; tracked as a follow-up.
152 dolt-backup:
153 # Pinned to the SAME digest as the doltgres service (2026-07-07 window) —
154 # the sidecar's psql must always match the server's engine version.
155 image: dolthub/doltgresql@sha256:0483137d0309598d3b0c111dff85d565077bd91cb0524ce00bb832929d5d5ddc
156 restart: unless-stopped
157 logging: *briven-logging
158 depends_on:
159 doltgres:
160 condition: service_healthy
161 environment:
162 PGHOST: doltgres
163 PGPORT: '5432'
164 PGUSER: postgres
165 PGPASSWORD: ${BRIVEN_DOLTGRES_PASSWORD}
166 BRIVEN_BACKUP_INTERVAL_SECONDS: ${BRIVEN_BACKUP_INTERVAL_SECONDS:-86400}
167 volumes:
168 - doltgres_backups:/backups
169 # Read-only view of the server's data dir, ONLY so auth.db (the engine's
170 # users/grants file — corrupted once on 2026-07-07, nothing backed it up)
171 # can be snapshotted alongside the dolt backups below.
172 - doltgres_data:/doltgres-data:ro
173 entrypoint: ['/bin/sh', '-c']
174 command:
175 - |
176 set -eu
177 echo "dolt-backup: starting (interval=${BRIVEN_BACKUP_INTERVAL_SECONDS:-86400}s)"
178 while true; do
179 ts="$$(date -u +%Y-%m-%dT%H:%M:%SZ)"
180 echo "[dolt-backup $$ts] enumerating data-plane databases"
181 # All non-template databases on the doltgres server (one per project,
182 # plus the default `postgres`). -tA = tuples only, unaligned.
183 dbs="$$(psql -tA -d postgres -c \
184 "SELECT datname FROM pg_database WHERE datname NOT IN ('template0','template1')")"
185 for db in $$dbs; do
186 echo "[dolt-backup $$ts] backing up $$db -> file:///backups/$$db"
187 if psql -d "$$db" -c \
188 "SELECT dolt_backup('sync-url', 'file:///backups/$$db');" >/dev/null; then
189 echo "[dolt-backup $$ts] ok $$db"
190 else
191 echo "[dolt-backup $$ts] WARN backup failed for $$db"
192 fi
193 done
194 # auth.db snapshot — tiny file, changes only on role/grant edits.
195 # Keep the newest 14 copies (2 weeks at the daily default interval).
196 if [ -f /doltgres-data/auth.db ]; then
197 mkdir -p /backups/auth-db
198 if cp /doltgres-data/auth.db "/backups/auth-db/auth.db.$$ts"; then
199 echo "[dolt-backup $$ts] ok auth.db snapshot"
200 ls -1t /backups/auth-db | tail -n +15 | while read -r old; do
201 rm -f "/backups/auth-db/$$old"
202 done
203 else
204 echo "[dolt-backup $$ts] WARN auth.db snapshot failed"
205 fi
206 else
207 echo "[dolt-backup $$ts] WARN auth.db not found in data dir"
208 fi
209 echo "[dolt-backup $$ts] run complete; sleeping"
210 sleep "$${BRIVEN_BACKUP_INTERVAL_SECONDS:-86400}"
211 done
212 networks:
213 - briven
214 labels:
215 - 'briven_logs=true'
216
217 # ─── briven-engine Auth DB (DOLTGRES ONLY — non-negotiable) ───────────
218 # HARD RULE: the COMPLETE Briven project is Doltgres. New parts do not get
219 # a separate stock-Postgres brain. SuperTokens Core Docker is REMOVED —
220 # it is incompatible with Doltgres (SET SESSION CHARACTERISTICS).
221 # briven-engine = Briven API code + tables in Doltgres DB `briven_engine`.
222 # Create/migrate: API `ensureBrivenEngineDatabase` + schema bootstrap.
223 briven-engine-db-init:
224 image: dolthub/doltgresql@sha256:0483137d0309598d3b0c111dff85d565077bd91cb0524ce00bb832929d5d5ddc
225 restart: 'no'
226 logging: *briven-logging
227 depends_on:
228 doltgres:
229 condition: service_healthy
230 environment:
231 PGPASSWORD: ${BRIVEN_DOLTGRES_PASSWORD}
232 entrypoint:
233 - /bin/sh
234 - -c
235 - |
236 set -e
237 echo "briven-engine-db-init: ensuring briven_engine on DOLTGRES only…"
238 exists=$$(psql -h doltgres -U postgres -d postgres -tAc "SELECT 1 FROM pg_database WHERE datname='briven_engine'" || true)
239 if [ "$$exists" = "1" ]; then
240 echo "briven_engine already exists on doltgres"
241 else
242 psql -h doltgres -U postgres -d postgres -c 'CREATE DATABASE briven_engine'
243 echo "created briven_engine on doltgres"
244 fi
245 networks:
246 - briven
247 labels:
248 - 'briven.service=briven-engine-db-init'
249 - 'briven.db=doltgres'
250
251 # ─── shared infra ─────────────────────────────────────────────────────
252 redis:
253 image: redis:7.4-alpine
254 restart: unless-stopped
255 logging: *briven-logging
256 command: redis-server --appendonly yes
257 volumes:
258 - redis_data:/data
259 healthcheck:
260 test: ['CMD-SHELL', 'redis-cli ping | grep -q PONG']
261 interval: 10s
262 timeout: 5s
263 retries: 5
264 start_period: 10s
265 networks:
266 - briven
267 labels:
268 - 'briven_logs=true'
269
270 minio:
271 image: minio/minio:latest
272 restart: unless-stopped
273 logging: *briven-logging
274 command: server /data --console-address ':9001'
275 environment:
276 MINIO_ROOT_USER: briven
277 MINIO_ROOT_PASSWORD: ${BRIVEN_MINIO_ROOT_PASSWORD}
278 volumes:
279 - minio_data:/data
280 healthcheck:
281 test: ['CMD-SHELL', 'curl -fsS http://localhost:9000/minio/health/live || exit 1']
282 interval: 15s
283 timeout: 5s
284 retries: 5
285 start_period: 20s
286 networks:
287 - briven
288 - dokploy-network
289 labels:
290 - 'briven_logs=true'
291 - 'traefik.enable=true'
292 - 'traefik.docker.network=dokploy-network'
293 # Public S3 endpoint — browsers PUT/GET with sigv4-presigned URLs the
294 # api mints. The api also reaches MinIO internally at http://minio:9000.
295 - 'traefik.http.routers.briven-s3.rule=Host(`s3.${BRIVEN_DOMAIN}`)'
296 - 'traefik.http.routers.briven-s3.entrypoints=websecure'
297 - 'traefik.http.routers.briven-s3.tls.certresolver=letsencrypt'
298 - 'traefik.http.routers.briven-s3.service=briven-s3'
299 - 'traefik.http.services.briven-s3.loadbalancer.server.port=9000'
300
301 # One-shot bucket creator. `mc mb --ignore-existing` is idempotent, so this
302 # runs every deploy and no-ops after the first. restart: 'no' = one-shot, so
303 # per infra/CLAUDE.md it does NOT need the logging cap.
304 minio-init:
305 image: minio/mc:latest
306 depends_on:
307 minio:
308 condition: service_healthy
309 entrypoint: >
310 /bin/sh -c "
311 until /usr/bin/mc alias set minio http://minio:9000 briven ${BRIVEN_MINIO_ROOT_PASSWORD} >/dev/null 2>&1; do
312 echo 'waiting for minio...'; sleep 2;
313 done;
314 /usr/bin/mc mb --ignore-existing minio/${BRIVEN_MINIO_BUCKET:-briven};
315 echo 'minio bucket ready: ${BRIVEN_MINIO_BUCKET:-briven}';
316 "
317 restart: 'no'
318 networks:
319 - briven
320
321 # ─── application services ─────────────────────────────────────────────
322 api:
323 build:
324 context: ../..
325 dockerfile: apps/api/Dockerfile
326 restart: unless-stopped
327 logging: *briven-logging
328 depends_on:
329 postgres:
330 condition: service_healthy
331 doltgres:
332 condition: service_healthy
333 redis:
334 condition: service_healthy
335 briven-engine-db-init:
336 condition: service_completed_successfully
337 environment:
338 BRIVEN_ENV: production
339 BRIVEN_API_PORT: '3001'
340 BRIVEN_API_ORIGIN: https://api.${BRIVEN_DOMAIN}
341 BRIVEN_WEB_ORIGIN: https://${BRIVEN_DOMAIN}
342 BRIVEN_ADMIN_ORIGIN: https://admin.${BRIVEN_DOMAIN}
343 # briven-engine = API + Doltgres only (NO SuperTokens Core container).
344 BRIVEN_AUTH_CORE_ENABLED: ${BRIVEN_AUTH_CORE_ENABLED:-true}
345 # SQL for Auth vault — ALWAYS Doltgres DB briven_engine (DOLTGRES-FIRST).
346 BRIVEN_ENGINE_DATABASE_URL: postgres://postgres:${BRIVEN_DOLTGRES_PASSWORD}@doltgres:5432/briven_engine?sslmode=disable
347 BRIVEN_AUTH_ENABLED: ${BRIVEN_AUTH_ENABLED:-false}
348 # Observability stack (host-managed compose at /root/briven-observability
349 # on the France box, same dokploy-network) — powers admin host gauges,
350 # the live cpu chart, and /v1/admin/timeseries prom-backed series.
351 BRIVEN_PROMETHEUS_URL: ${BRIVEN_PROMETHEUS_URL:-}
352 # Hard allowlist: only these emails can EVER be platform admin
353 # (see apps/api/src/lib/superadmin.ts). Value lives in the Dokploy env.
354 BRIVEN_SUPERADMIN_EMAILS: ${BRIVEN_SUPERADMIN_EMAILS:-}
355 BRIVEN_TRUSTED_ORIGINS: https://${BRIVEN_DOMAIN},https://api.${BRIVEN_DOMAIN},https://admin.${BRIVEN_DOMAIN}
356 # Control plane — stock Postgres, postgres.js/drizzle.
357 # Control brain on Doltgres (same cluster as project DBs). Password is
358 # DOLTGRES, not the legacy stock-Postgres password.
359 BRIVEN_DATABASE_URL: postgres://postgres:${BRIVEN_DOLTGRES_PASSWORD}@doltgres:5432/briven_control?sslmode=disable
360 # Data plane — DoltGres, `pg` driver, database-per-project. The api
361 # connects to the default `postgres` database and CREATEs per-project
362 # databases on this server.
363 BRIVEN_DATA_PLANE_URL: postgres://postgres:${BRIVEN_DOLTGRES_PASSWORD}@doltgres:5432/postgres?sslmode=disable
364 BRIVEN_REDIS_URL: redis://redis:6379
365 BRIVEN_RUNTIME_URL: http://runtime:3003
366 # Without this the api falls back to localhost:3004 and can't reach
367 # realtime — surfaces as realtime_stats_failed "Unable to connect".
368 BRIVEN_REALTIME_URL: http://realtime:3004
369 BRIVEN_RUNTIME_SHARED_SECRET: ${BRIVEN_RUNTIME_SHARED_SECRET}
370 BRIVEN_BETTER_AUTH_SECRET: ${BRIVEN_BETTER_AUTH_SECRET}
371 BRIVEN_AUDIT_IP_PEPPER: ${BRIVEN_AUDIT_IP_PEPPER}
372 BRIVEN_ENCRYPTION_KEY: ${BRIVEN_ENCRYPTION_KEY}
373 # Per-tenant secret-store master key (ARCHITECTURE.md §4/§9).
374 # BRIVEN_AUTH_ENABLED is set once above with the engine env block.
375 # Values live in the Dokploy env panel; empty = unset in loadEnv.
376 BRIVEN_AUTH_MASTER_KEY: ${BRIVEN_AUTH_MASTER_KEY:-}
377 BRIVEN_MITTERA_API_URL: ${BRIVEN_MITTERA_API_URL:-}
378 BRIVEN_MITTERA_API_KEY: ${BRIVEN_MITTERA_API_KEY:-}
379 BRIVEN_MITTERA_WEBHOOK_SECRET: ${BRIVEN_MITTERA_WEBHOOK_SECRET:-}
380 # Auth OTP / magic-link real inbox: set HOST+USER+PASS(+FROM) for SMTP primary.
381 # Until SMTP is set, Auth uses mittera (same as platform mail).
382 BRIVEN_SMTP_HOST: ${BRIVEN_SMTP_HOST:-}
383 BRIVEN_SMTP_PORT: ${BRIVEN_SMTP_PORT:-587}
384 BRIVEN_SMTP_USER: ${BRIVEN_SMTP_USER:-}
385 BRIVEN_SMTP_PASS: ${BRIVEN_SMTP_PASS:-}
386 BRIVEN_SMTP_FROM: ${BRIVEN_SMTP_FROM:-}
387 BRIVEN_GOOGLE_CLIENT_ID: ${BRIVEN_GOOGLE_CLIENT_ID:-}
388 BRIVEN_GOOGLE_CLIENT_SECRET: ${BRIVEN_GOOGLE_CLIENT_SECRET:-}
389 BRIVEN_GITHUB_CLIENT_ID: ${BRIVEN_GITHUB_CLIENT_ID:-}
390 BRIVEN_GITHUB_CLIENT_SECRET: ${BRIVEN_GITHUB_CLIENT_SECRET:-}
391 BRIVEN_KONNOS_CLIENT_ID: ${BRIVEN_KONNOS_CLIENT_ID:-}
392 BRIVEN_KONNOS_CLIENT_SECRET: ${BRIVEN_KONNOS_CLIENT_SECRET:-}
393 BRIVEN_KONNOS_ISSUER: ${BRIVEN_KONNOS_ISSUER:-https://code.konnos.org}
394 BRIVEN_DISCORD_CLIENT_ID: ${BRIVEN_DISCORD_CLIENT_ID:-}
395 BRIVEN_DISCORD_CLIENT_SECRET: ${BRIVEN_DISCORD_CLIENT_SECRET:-}
396 BRIVEN_POLAR_API_BASE: ${BRIVEN_POLAR_API_BASE:-https://api.polar.sh}
397 BRIVEN_POLAR_ACCESS_TOKEN: ${BRIVEN_POLAR_ACCESS_TOKEN:-}
398 BRIVEN_POLAR_WEBHOOK_SECRET: ${BRIVEN_POLAR_WEBHOOK_SECRET:-}
399 BRIVEN_POLAR_PRO_PRODUCT_ID: ${BRIVEN_POLAR_PRO_PRODUCT_ID:-}
400 BRIVEN_POLAR_TEAM_PRODUCT_ID: ${BRIVEN_POLAR_TEAM_PRODUCT_ID:-}
401 BRIVEN_DOMAIN: ${BRIVEN_DOMAIN}
402 BRIVEN_OPEN_SIGNUPS: ${BRIVEN_OPEN_SIGNUPS:-false}
403 BRIVEN_OLLAMA_URL: ${BRIVEN_OLLAMA_URL:-}
404 BRIVEN_OLLAMA_API_KEY: ${BRIVEN_OLLAMA_API_KEY:-}
405 BRIVEN_OLLAMA_MODEL: ${BRIVEN_OLLAMA_MODEL:-qwen2.5-coder:32b}
406 BRIVEN_MINIO_ENDPOINT: http://minio:9000
407 BRIVEN_MINIO_PUBLIC_ENDPOINT: https://s3.${BRIVEN_DOMAIN}
408 BRIVEN_MINIO_ACCESS_KEY: briven
409 BRIVEN_MINIO_SECRET_KEY: ${BRIVEN_MINIO_ROOT_PASSWORD}
410 BRIVEN_MINIO_BUCKET: ${BRIVEN_MINIO_BUCKET:-briven}
411 BRIVEN_MINIO_REGION: ${BRIVEN_MINIO_REGION:-us-east-1}
412 # imgproxy — on-the-fly image transforms (M4). The api mints SIGNED
413 # imgproxy URLs (services/image-transform.ts). ENDPOINT is the
414 # media host + the /_t path prefix (imgproxy sits BEHIND media.<domain>
415 # under PathPrefix(/_t), no new subdomain). image-transform.ts signs the
416 # path WITHOUT the prefix (/rs:.../g:sm/<b64 source>) and imgproxy strips
417 # /_t before verifying — the signed portions match. KEY/SALT are the same
418 # hex secrets the imgproxy container gets; set all three in the Dokploy
419 # env to turn transforms on (unset => endpoint returns 503, fail-safe).
420 BRIVEN_IMGPROXY_ENDPOINT: https://media.${BRIVEN_DOMAIN}/_t
421 BRIVEN_IMGPROXY_KEY: ${BRIVEN_IMGPROXY_KEY:-}
422 BRIVEN_IMGPROXY_SALT: ${BRIVEN_IMGPROXY_SALT:-}
423 healthcheck:
424 # /info is documented to never 500 (apps/api/Dockerfile). bun ships in
425 # the api image and has a built-in fetch.
426 test: ['CMD-SHELL', "bun -e \"fetch('http://localhost:3001/info').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))\""]
427 interval: 15s
428 timeout: 5s
429 retries: 5
430 start_period: 40s
431 networks:
432 - briven
433 - dokploy-network
434 labels:
435 - 'briven_logs=true'
436 - 'traefik.enable=true'
437 - 'traefik.docker.network=dokploy-network'
438 - 'traefik.http.routers.briven-api.rule=Host(`api.${BRIVEN_DOMAIN}`)'
439 - 'traefik.http.routers.briven-api.entrypoints=websecure'
440 - 'traefik.http.routers.briven-api.tls.certresolver=letsencrypt'
441 - 'traefik.http.routers.briven-api.service=briven-api'
442 - 'traefik.http.services.briven-api.loadbalancer.server.port=3001'
443 # media.briven.tech -> this api (public file bytes). Explicit .service= on
444 # every router so Traefik does not treat the container as multi-service.
445 - 'traefik.http.routers.briven-media.rule=Host(`media.${BRIVEN_DOMAIN}`)'
446 - 'traefik.http.routers.briven-media.entrypoints=websecure'
447 - 'traefik.http.routers.briven-media.tls.certresolver=letsencrypt'
448 - 'traefik.http.routers.briven-media.service=briven-api'
449 # /_t image transforms: router lives on the imgproxy service (below), not here.
450
451 # imgproxy — on-the-fly image transforms (M4). Sits BEHIND media.<domain>
452 # under the /_t path prefix (no new public subdomain, per the media-host
453 # rule). The api mints signed URLs; imgproxy fetches the source from the
454 # PUBLIC media host, resizes, and returns it. SSRF is locked to the media
455 # host via IMGPROXY_ALLOWED_SOURCES. No host port — only Traefik (on the
456 # dokploy-network) can reach port 8080.
457 imgproxy:
458 image: darthsim/imgproxy:latest
459 restart: unless-stopped
460 logging: *briven-logging
461 environment:
462 # Signing — MUST equal the api's BRIVEN_IMGPROXY_KEY/SALT (same hex
463 # secrets, set in the Dokploy env). imgproxy strips IMGPROXY_PATH_PREFIX
464 # (/_t) from the request path, then verifies the HMAC over the remaining
465 # /rs:.../g:sm/<b64 source> — exactly what image-transform.ts signed.
466 IMGPROXY_KEY: ${BRIVEN_IMGPROXY_KEY:-}
467 IMGPROXY_SALT: ${BRIVEN_IMGPROXY_SALT:-}
468 IMGPROXY_PATH_PREFIX: /_t
469 # SSRF guard: imgproxy may ONLY fetch sources from the public media host.
470 IMGPROXY_ALLOWED_SOURCES: https://media.${BRIVEN_DOMAIN}/
471 # Sane limits — resize-bomb / oversized-source guards. Format auto-nego:
472 # serve WebP to browsers that send Accept: image/webp.
473 IMGPROXY_MAX_SRC_RESOLUTION: '50'
474 IMGPROXY_ENABLE_WEBP_DETECTION: 'true'
475 # Bind on 8080 (default) — Traefik reaches it over dokploy-network only.
476 IMGPROXY_BIND: ':8080'
477 healthcheck:
478 # imgproxy ships an /health endpoint that never 500s when the process
479 # is up. imgproxy has no shell/curl, so use its built-in health probe.
480 test: ['CMD', 'imgproxy', 'health']
481 interval: 15s
482 timeout: 5s
483 retries: 5
484 start_period: 20s
485 networks:
486 - dokploy-network
487 labels:
488 - 'briven_logs=true'
489 - 'traefik.enable=true'
490 - 'traefik.docker.network=dokploy-network'
491 - 'traefik.http.services.briven-imgproxy.loadbalancer.server.port=8080'
492 # Higher priority than briven-media so /_t/* hits imgproxy; rest of
493 # media.<domain> stays on the api. IMGPROXY_PATH_PREFIX=/_t strips prefix.
494 - 'traefik.http.routers.briven-imgt.rule=Host(`media.${BRIVEN_DOMAIN}`) && PathPrefix(`/_t`)'
495 - 'traefik.http.routers.briven-imgt.priority=100'
496 - 'traefik.http.routers.briven-imgt.entrypoints=websecure'
497 - 'traefik.http.routers.briven-imgt.tls.certresolver=letsencrypt'
498 - 'traefik.http.routers.briven-imgt.service=briven-imgproxy'
499
500 runtime:
501 build:
502 context: ../..
503 dockerfile: apps/runtime/Dockerfile
504 restart: unless-stopped
505 logging: *briven-logging
506 depends_on:
507 api:
508 condition: service_started
509 doltgres:
510 condition: service_healthy
511 environment:
512 BRIVEN_ENV: production
513 BRIVEN_RUNTIME_PORT: '3003'
514 BRIVEN_RUNTIME_SHARED_SECRET: ${BRIVEN_RUNTIME_SHARED_SECRET}
515 BRIVEN_RUNTIME_EXECUTOR: deno
516 BRIVEN_RUNTIME_BUNDLE_DIR: /var/lib/briven/bundles
517 BRIVEN_API_INTERNAL_URL: http://api:3001
518 # Data plane — DoltGres (the old BRIVEN_URL mysql:// was wrong, removed).
519 BRIVEN_DATA_PLANE_URL: postgres://postgres:${BRIVEN_DOLTGRES_PASSWORD}@doltgres:5432/postgres?sslmode=disable
520 volumes:
521 - runtime_bundles:/var/lib/briven/bundles
522 healthcheck:
523 # Any HTTP response = process is up (port serving).
524 test: ['CMD-SHELL', "bun -e \"fetch('http://localhost:3003/').then(()=>process.exit(0)).catch(()=>process.exit(1))\""]
525 interval: 15s
526 timeout: 5s
527 retries: 5
528 start_period: 40s
529 networks:
530 - briven
531 labels:
532 - 'briven_logs=true'
533
534 realtime:
535 build:
536 context: ../..
537 dockerfile: apps/realtime/Dockerfile
538 restart: unless-stopped
539 logging: *briven-logging
540 depends_on:
541 doltgres:
542 condition: service_healthy
543 environment:
544 BRIVEN_ENV: production
545 BRIVEN_REALTIME_PORT: '3004'
546 BRIVEN_API_INTERNAL_URL: http://api:3001
547 BRIVEN_RUNTIME_SHARED_SECRET: ${BRIVEN_RUNTIME_SHARED_SECRET}
548 # Data plane — DoltGres. Realtime polls DOLT_HASHOF('HEAD') per project
549 # (no LISTEN/NOTIFY on DoltGres). The old BRIVEN_URL mysql:// was wrong.
550 BRIVEN_DATA_PLANE_URL: postgres://postgres:${BRIVEN_DOLTGRES_PASSWORD}@doltgres:5432/postgres?sslmode=disable
551 BRIVEN_REALTIME_POLL_MS: '500'
552 healthcheck:
553 test: ['CMD-SHELL', "bun -e \"fetch('http://localhost:3004/').then(()=>process.exit(0)).catch(()=>process.exit(1))\""]
554 interval: 15s
555 timeout: 5s
556 retries: 5
557 start_period: 40s
558 networks:
559 - briven
560 - dokploy-network
561 labels:
562 - 'briven_logs=true'
563 - 'traefik.enable=true'
564 - 'traefik.docker.network=dokploy-network'
565 - 'traefik.http.routers.briven-realtime.rule=Host(`realtime.${BRIVEN_DOMAIN}`)'
566 - 'traefik.http.routers.briven-realtime.entrypoints=websecure'
567 - 'traefik.http.routers.briven-realtime.tls.certresolver=letsencrypt'
568 - 'traefik.http.services.briven-realtime.loadbalancer.server.port=3004'
569
570 web:
571 build:
572 context: ../..
573 dockerfile: apps/web/Dockerfile
574 restart: unless-stopped
575 logging: *briven-logging
576 depends_on:
577 api:
578 condition: service_started
579 environment:
580 BRIVEN_API_ORIGIN: https://api.${BRIVEN_DOMAIN}
581 BRIVEN_WEB_ORIGIN: https://${BRIVEN_DOMAIN}
582 NEXT_PUBLIC_BRIVEN_API_ORIGIN: https://api.${BRIVEN_DOMAIN}
583 NEXT_PUBLIC_BRIVEN_HAS_GOOGLE_OAUTH: ${BRIVEN_GOOGLE_CLIENT_ID:+true}
584 NEXT_PUBLIC_BRIVEN_HAS_GITHUB_OAUTH: ${BRIVEN_GITHUB_CLIENT_ID:+true}
585 NEXT_PUBLIC_BRIVEN_HAS_KONNOS_OAUTH: ${BRIVEN_KONNOS_CLIENT_ID:+true}
586 NEXT_PUBLIC_BRIVEN_HAS_DISCORD_OAUTH: ${BRIVEN_DISCORD_CLIENT_ID:+true}
587 healthcheck:
588 # web runs `next start` on node — use node's built-in fetch.
589 test: ['CMD-SHELL', "node -e \"fetch('http://localhost:3000/').then(()=>process.exit(0)).catch(()=>process.exit(1))\""]
590 interval: 15s
591 timeout: 5s
592 retries: 5
593 start_period: 40s
594 networks:
595 - briven
596 - dokploy-network
597 labels:
598 - 'briven_logs=true'
599 - 'traefik.enable=true'
600 - 'traefik.docker.network=dokploy-network'
601 - 'traefik.http.routers.briven-web.rule=Host(`${BRIVEN_DOMAIN}`) || Host(`app.${BRIVEN_DOMAIN}`) || Host(`admin.${BRIVEN_DOMAIN}`)'
602 - 'traefik.http.routers.briven-web.entrypoints=websecure'
603 - 'traefik.http.routers.briven-web.tls.certresolver=letsencrypt'
604 - 'traefik.http.routers.briven-web.service=briven-web'
605 - 'traefik.http.services.briven-web.loadbalancer.server.port=3000'
606
607 docs:
608 build:
609 context: ../..
610 dockerfile: apps/docs/Dockerfile
611 restart: unless-stopped
612 logging: *briven-logging
613 depends_on:
614 api:
615 condition: service_started
616 environment:
617 # Used by /status + /api/status/incidents.xml to read live incidents.
618 BRIVEN_API_ORIGIN: https://api.${BRIVEN_DOMAIN}
619 healthcheck:
620 test: ['CMD-SHELL', "node -e \"fetch('http://localhost:3002/').then(()=>process.exit(0)).catch(()=>process.exit(1))\""]
621 interval: 15s
622 timeout: 5s
623 retries: 5
624 start_period: 40s
625 networks:
626 - briven
627 - dokploy-network
628 labels:
629 - 'briven_logs=true'
630 - 'traefik.enable=true'
631 - 'traefik.docker.network=dokploy-network'
632 - 'traefik.http.routers.briven-docs.rule=Host(`docs.${BRIVEN_DOMAIN}`)'
633 - 'traefik.http.routers.briven-docs.entrypoints=websecure'
634 - 'traefik.http.routers.briven-docs.tls.certresolver=letsencrypt'
635 - 'traefik.http.routers.briven-docs.service=briven-docs'
636 - 'traefik.http.services.briven-docs.loadbalancer.server.port=3002'
637 # status.${BRIVEN_DOMAIN} — same docs container; bare `/` rewrites to
638 # /status. Other paths pass through (so /api/status/incidents.xml works).
639 - 'traefik.http.routers.briven-status.rule=Host(`status.${BRIVEN_DOMAIN}`)'
640 - 'traefik.http.routers.briven-status.entrypoints=websecure'
641 - 'traefik.http.routers.briven-status.tls.certresolver=letsencrypt'
642 - 'traefik.http.routers.briven-status.service=briven-docs'
643 - 'traefik.http.routers.briven-status.middlewares=briven-status-rewrite'
644 - 'traefik.http.middlewares.briven-status-rewrite.replacepathregex.regex=^/$$'
645 - 'traefik.http.middlewares.briven-status-rewrite.replacepathregex.replacement=/status'
646
647volumes:
648 postgres_data:
649 doltgres_data:
650 doltgres_backups:
651 redis_data:
652 minio_data:
653 runtime_bundles:
654
655networks:
656 briven:
657 driver: bridge
658 # Dokploy's ingress network — Traefik watches this for routing + TLS.
659 # Routed services (api, realtime, web, docs, s3) attach to it in addition
660 # to the internal `briven` network; DBs/redis/runtime stay internal-only.
661 dokploy-network:
662 external: true