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
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: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
578volumes:
579 postgres_data:
580 doltgres_data:
581 doltgres_backups:
582 redis_data:
583 minio_data:
584 runtime_bundles:
585
586networks:
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