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 | |
| 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@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 | |
| 647 | volumes: |
| 648 | postgres_data: |
| 649 | doltgres_data: |
| 650 | doltgres_backups: |
| 651 | redis_data: |
| 652 | minio_data: |
| 653 | runtime_bundles: |
| 654 | |
| 655 | networks: |
| 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 |