-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.dev.yml
More file actions
522 lines (505 loc) · 25.9 KB
/
Copy pathdocker-compose.dev.yml
File metadata and controls
522 lines (505 loc) · 25.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
# Agora — LOCAL DEV compose. Brings up the backing services + the supporting containers
# (proxy · cron · scorer) and points every one of them BACK at your host-run dev code:
#
# agora (API) ............ pnpm --filter @agora/api dev → host :4000
# secure-chat ............ pnpm --filter @agora/secure-chat dev → host :4002
# admin (SPA) ............ DEV_HOST=true pnpm --filter @agora/admin dev → host :5173
#
# NB the admin vite server MUST be started with DEV_HOST=true here — that binds it to 0.0.0.0 so the
# Caddy container can reach it via host.docker.internal:5173. A plain `pnpm dev` binds loopback-only
# (localhost) and the proxy gets `connection refused` → 502 on `/`.
#
# i.e. the inverse of docker-compose.yml: the THREE app surfaces YOU edit run natively (instant tsx /
# vite HMR, breakpoints, no rebuild), and the containers (Caddy front door, cron scheduler, the Python
# scorer + its model servers, Postgres/MinIO/Redis/Neo4j) talk to them over `host.docker.internal`.
# The proxy is a real front door: hit http://localhost/ and you get the live admin, /v7 → host API,
# /v7/.../secure-chat + /secure-socket/ → host secure-chat, /moderator → scorer, /media → MinIO, all
# same-origin. Run it with the dedicated `-f`:
#
# docker compose -f docker-compose.dev.yml --profile selfhost --profile full up --build
#
# ── SAME TWO-AXIS PROFILE MODEL as docker-compose.yml (a bare `up` starts NOTHING) ────────────────
# Axis 1 — DATA PLANE (pick ONE):
# --profile supabase external Supabase Postgres + Storage (no db/minio container)
# --profile selfhost local Postgres (db) + MinIO — fully offline dev
# Axis 2 — OPTIONAL ADD-ONS (compose freely):
# --profile scorer the Python moderation + social-graph stack: scorer (×3) + neo4j
# --profile secure-chat Redis (the secure-chat suspension index — a HARD dep of the host secure-chat)
# --profile scale Redis as the cross-replica rate-limit store
# --profile full shorthand for ALL add-ons (= scorer + secure-chat)
# --profile demo the 1:1 SDK compatibility harness behind the front door at /demo/
# --profile observability Grafana Alloy + LGTM (flip OTEL_SDK_DISABLED=false in .env to export)
#
# ── WHAT'S DIFFERENT vs docker-compose.yml (besides dropping the agora / secure-chat containers) ───
# • Every container that calls the app talks to the HOST: API_UPSTREAM / SECURE_CHAT_UPSTREAM /
# ADMIN_UPSTREAM, the scorer's API_BASE_URL, and cron's AGORA_URL / SECURE_CHAT_URL all resolve to
# `host.docker.internal` (extra_hosts maps it to host-gateway so this works on Linux too, not just
# Docker Desktop).
# • Backing ports are PUBLISHED to the host (redis :6379, db :5432, minio :9000/:9001, neo4j
# :7474/:7687) so your `pnpm dev` processes can reach them at localhost. Start from `cp
# .env.dev.example .env` — it defaults to a LOCAL Postgres + MinIO with host-side localhost URLs
# (DATABASE_URL=…@localhost:5432, REDIS_URL=redis://localhost:6379, NEO4J_URI=bolt://localhost:7687,
# S3_ENDPOINT=http://localhost:9000) and DEV_DATABASE_URL=…@db:5432 for the scorer container. Cloud
# Supabase is the commented in-file switch (then use --profile supabase instead of --profile selfhost).
# • The scorer-worker reaches the API + Neo4j at fixed in-network addresses (host.docker.internal:4000,
# neo4j:7687), so no host-vs-container override is needed for those. For the DB it uses the standard
# DATABASE_URL — on supabase the cloud URL works from the container as-is. The one case the host URL
# doesn't carry into the container is selfhost (host DATABASE_URL is localhost:5432, which inside a
# container is the container): set DEV_DATABASE_URL=…@db:5432/postgres and the worker uses that
# instead (mirrors TEST_DATABASE_URL; falls back to DATABASE_URL when unset).
#
# Isolated project name so this stack's containers/volumes/network never collide with a
# docker-compose.yml stack you may also have up.
name: agora-dev
services:
# ── backing services ─────────────────────────────────────────────────────────────────────────────
# Shared Redis — the secure-chat suspension index (hard dep of the host secure-chat process) + the
# optional cross-replica rate-limit store. PUBLISHED to the host so `pnpm dev` reaches it at
# localhost:6379 (set REDIS_URL=redis://localhost:6379 in .env).
redis:
image: redis:7-alpine
profiles: ["secure-chat", "scale", "full"]
command: ["redis-server", "--save", "", "--maxmemory", "128mb", "--maxmemory-policy", "allkeys-lru"]
ports:
- "${REDIS_PORT:-6379}:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
init: true
restart: unless-stopped
# Postgres for offline dev (selfhost). Same supabase/postgres image the migrations assume (pgvector +
# PostGIS + pgmq + auth roles). Published on :5432 → host DATABASE_URL=…@localhost:5432. Apply
# migrations from the host: `cd apps/api && pnpm db:migrate:run` (it reads your host .env).
db:
image: supabase/postgres:15.8.1.060
profiles: ["selfhost"]
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-please_change_me}
ports:
- "${DB_PORT:-5432}:5432"
volumes:
- db-data:/var/lib/postgresql/data
# First-boot only: give supabase_auth_admin a password (the image doesn't derive one from
# POSTGRES_PASSWORD; the opt-in gotrue service below needs it). Existing volumes: see the
# file's header — apply the one-time ALTER ROLE by hand.
- ./deploy/db/init-auth-role.sql:/docker-entrypoint-initdb.d/init-scripts/99-agora-auth-role.sql:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 10
start_period: 20s
init: true
restart: unless-stopped
# MinIO — S3-compatible storage for offline dev (STORAGE_PROVIDER=s3). Published on :9000 (API) /
# :9001 (console) → host S3_ENDPOINT=http://localhost:9000. The host API creates the bucket +
# public-read policy on first upload; the Caddy front door serves public objects at /media.
minio:
image: minio/minio:latest
profiles: ["selfhost"]
command: ["server", "/data", "--console-address", ":9001"]
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-agora}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-please_change_me}
ports:
- "${MINIO_PORT:-9000}:9000"
- "${MINIO_CONSOLE_PORT:-9001}:9001"
volumes:
- minio-data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/ready"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
init: true
restart: unless-stopped
# GoTrue — Supabase Auth, self-hosted (SSO). OPT-IN in dev: the api runs on the HOST here, so it
# reaches GoTrue through the proxy's published shim (SUPABASE_URL=http://localhost:9998), while the
# BROWSER uses the public route (http://localhost/auth/v1/*). :9999 is published too, purely for
# curl/debugging against GoTrue directly (health, admin API).
#
# Unlike the container deploys, GOTRUE_JWT_SECRET has NO `:?` guard here — an unset secret must not
# break a plain `docker compose -f docker-compose.dev.yml --profile selfhost up` for devs who never
# touch SSO. Set the trio (node apps/api/scripts/gen-gotrue-keys.mjs) before using it; see
# .env.dev.example → "SSO in dev".
gotrue:
image: supabase/auth:v2.170.0
profiles: ["selfhost"]
depends_on:
db:
condition: service_healthy
environment:
GOTRUE_API_HOST: 0.0.0.0
GOTRUE_API_PORT: "9999"
GOTRUE_DB_DRIVER: postgres
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD:-please_change_me}@db:5432/postgres
API_EXTERNAL_URL: ${GOTRUE_EXTERNAL_URL:-http://localhost/auth/v1}
GOTRUE_SITE_URL: ${GOTRUE_SITE_URL:-http://localhost:5175}
GOTRUE_URI_ALLOW_LIST: ${GOTRUE_URI_ALLOW_LIST:-}
GOTRUE_JWT_SECRET: ${GOTRUE_JWT_SECRET:-}
GOTRUE_JWT_EXP: "3600"
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
GOTRUE_JWT_ADMIN_ROLES: service_role
GOTRUE_JWT_AUD: authenticated
GOTRUE_EXTERNAL_EMAIL_ENABLED: "true"
GOTRUE_DISABLE_SIGNUP: "false"
# Dev default: no SMTP, sign-ups confirm instantly.
GOTRUE_MAILER_AUTOCONFIRM: ${GOTRUE_MAILER_AUTOCONFIRM:-true}
GOTRUE_MAILER_URLPATHS_CONFIRMATION: /auth/v1/verify
GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/v1/verify
GOTRUE_MAILER_URLPATHS_INVITE: /auth/v1/verify
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/v1/verify
# Social providers — off until configured. Redirect URI is always <API_EXTERNAL_URL>/callback.
GOTRUE_EXTERNAL_GOOGLE_ENABLED: ${GOTRUE_EXTERNAL_GOOGLE_ENABLED:-false}
GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID: ${GOTRUE_EXTERNAL_GOOGLE_CLIENT_ID:-}
GOTRUE_EXTERNAL_GOOGLE_SECRET: ${GOTRUE_EXTERNAL_GOOGLE_SECRET:-}
GOTRUE_EXTERNAL_GOOGLE_REDIRECT_URI: ${GOTRUE_EXTERNAL_URL:-http://localhost/auth/v1}/callback
GOTRUE_EXTERNAL_GITHUB_ENABLED: ${GOTRUE_EXTERNAL_GITHUB_ENABLED:-false}
GOTRUE_EXTERNAL_GITHUB_CLIENT_ID: ${GOTRUE_EXTERNAL_GITHUB_CLIENT_ID:-}
GOTRUE_EXTERNAL_GITHUB_SECRET: ${GOTRUE_EXTERNAL_GITHUB_SECRET:-}
GOTRUE_EXTERNAL_GITHUB_REDIRECT_URI: ${GOTRUE_EXTERNAL_URL:-http://localhost/auth/v1}/callback
GOTRUE_EXTERNAL_APPLE_ENABLED: ${GOTRUE_EXTERNAL_APPLE_ENABLED:-false}
GOTRUE_EXTERNAL_APPLE_CLIENT_ID: ${GOTRUE_EXTERNAL_APPLE_CLIENT_ID:-}
GOTRUE_EXTERNAL_APPLE_SECRET: ${GOTRUE_EXTERNAL_APPLE_SECRET:-}
GOTRUE_EXTERNAL_APPLE_REDIRECT_URI: ${GOTRUE_EXTERNAL_URL:-http://localhost/auth/v1}/callback
ports:
- "${GOTRUE_PORT:-9999}:9999"
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9999/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 15s
init: true
restart: unless-stopped
# Neo4j / DozerDB — social-graph DB. PUBLISHED (:7474 browser, :7687 Bolt) so the host API reaches it
# at bolt://localhost:7687 (NEO4J_URI in .env). The scorer-worker container reaches it in-network at the
# fixed bolt://neo4j:7687. Rides the `scorer` profile (scorer-worker hard-deps it).
neo4j:
# `neo4j` profile brings up just the graph DB (no scorer model servers) — handy for graph
# dev / running the API's /social/* reads against a live graph without the full scorer stack.
profiles: ["scorer", "full", "neo4j"]
image: graphstack/dozerdb:5.26.3.0
environment:
NEO4J_AUTH: ${NEO4J_AUTH:-neo4j/please_change_me}
NEO4J_ACCEPT_LICENSE_AGREEMENT: ${NEO4J_ACCEPT_LICENSE_AGREEMENT:-yes}
NEO4J_initial_dbms_default__database: ${NEO4J_initial_dbms_default__database:-${NEO4J_DATABASE:-neo4j}}
NEO4J_PLUGINS: '["apoc"]'
NEO4J_apoc_export_file_enabled: ${NEO4J_apoc_export_file_enabled:-true}
NEO4J_apoc_import_file_enabled: ${NEO4J_apoc_import_file_enabled:-true}
NEO4J_dbms_security_procedures_unrestricted: ${NEO4J_dbms_security_procedures_unrestricted:-gds.*,apoc.*}
NEO4J_dbms_security_procedures_allowlist: ${NEO4J_dbms_security_procedures_allowlist:-gds.*,apoc.*}
NEO4J_server_memory_heap_initial__size: ${NEO4J_server_memory_heap_initial__size:-512m}
NEO4J_server_memory_heap_max__size: ${NEO4J_server_memory_heap_max__size:-1024m}
NEO4J_server_memory_pagecache_size: ${NEO4J_server_memory_pagecache_size:-256m}
ports:
- "7474:7474"
- "7687:7687"
volumes:
- neo4j-data:/data
- ./neo4j/plugins:/plugins # OpenGDS jar (open-gds-*.jar) — loaded at startup
healthcheck:
test: ["CMD-SHELL", "cypher-shell -u \"$${NEO4J_AUTH%%/*}\" -p \"$${NEO4J_AUTH#*/}\" 'RETURN 1' >/dev/null 2>&1 || exit 1"]
interval: 10s
timeout: 10s
retries: 12
start_period: 40s
init: true
restart: unless-stopped
# ── the Python scorer (the `scorer` profile) — built from source, like docker-compose.yml ─────────
# Two RoBERTa model servers (warm in RAM) + the orchestrator. The orchestrator writes moderation
# verdicts back to your HOST API over HTTP, and reads/writes the same Postgres (pgmq) the host API does.
scorer-toxicity:
profiles: ["scorer", "full"]
build:
context: .
dockerfile: services/scorer/Dockerfile.model-server
image: agora-scorer-model
env_file: .env
environment:
SCORER_MODEL: ${SCORER_TOXICITY_MODEL:-s-nlp/roberta_toxicity_classifier}
SCORER_MODEL_KIND: toxicity
SCORER_PORT: "8001"
OMP_NUM_THREADS: "1"
LOG_LEVEL: ${LOG_LEVEL:-info}
# Traces push OTLP to alloy; metrics are exposed on :9464 for alloy to SCRAPE (see
# deploy/observability/config.alloy's scorer scrape job) — never pushed, so no 404s against
# alloy's OTLP receiver (which intentionally drops metrics to avoid double-counting).
OTEL_SDK_DISABLED: ${OTEL_SDK_DISABLED:-true}
OTEL_SERVICE_NAME: agora-scorer-toxicity
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://alloy:4318}
cpuset: "0"
mem_limit: "1.5g"
init: true
restart: unless-stopped
volumes:
- scorer-hf-cache:/home/scorer/.cache/huggingface
scorer-relationship:
profiles: ["scorer", "full"]
build:
context: .
dockerfile: services/scorer/Dockerfile.model-server
image: agora-scorer-model
env_file: .env
environment:
SCORER_MODEL: ${SCORER_RELATIONSHIP_MODEL:-cardiffnlp/twitter-roberta-base-sentiment-latest}
SCORER_MODEL_KIND: relationship
SCORER_PORT: "8002"
OMP_NUM_THREADS: "1"
LOG_LEVEL: ${LOG_LEVEL:-info}
# Traces push OTLP to alloy; metrics are exposed on :9464 for alloy to SCRAPE — see the toxicity
# service's comment above (same story, different model).
OTEL_SDK_DISABLED: ${OTEL_SDK_DISABLED:-true}
OTEL_SERVICE_NAME: agora-scorer-relationship
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://alloy:4318}
cpuset: "1"
mem_limit: "1.5g"
init: true
restart: unless-stopped
volumes:
- scorer-hf-cache:/home/scorer/.cache/huggingface
# Orchestrator: pgmq consumer + write-back + Neo4j edges + the operator-gated admin API on :4001.
# • API_BASE_URL → the HOST API (write-back of moderation verdicts); fixed at host.docker.internal.
# • DATABASE_URL → DEV_DATABASE_URL if set, else the standard DATABASE_URL (mirrors TEST_DATABASE_URL).
# On the supabase data plane the cloud DATABASE_URL works from the container as-is, so nothing to set.
# On selfhost, your host .env's DATABASE_URL is localhost:5432 (which inside a container is the
# container itself), so point DEV_DATABASE_URL at the in-network db: …@db:5432/postgres.
# • NEO4J_URI → the in-network neo4j service (fixed), not the host's localhost value.
scorer-worker:
profiles: ["scorer", "full"]
build:
context: .
dockerfile: services/scorer/Dockerfile.worker
image: agora-scorer-worker
env_file: .env
environment:
API_BASE_URL: http://host.docker.internal:4000
DATABASE_URL: ${DEV_DATABASE_URL:-${DATABASE_URL}}
SCORER_TOXICITY_URL: http://scorer-toxicity:8001
SCORER_RELATIONSHIP_URL: http://scorer-relationship:8002
SCORER_ADMIN_PORT: "4001"
NEO4J_URI: bolt://neo4j:7687
LOG_LEVEL: ${LOG_LEVEL:-info}
# Traces push OTLP to alloy; metrics are exposed on :9464 for alloy to SCRAPE — see the toxicity
# service's comment above.
OTEL_SDK_DISABLED: ${OTEL_SDK_DISABLED:-true}
OTEL_SERVICE_NAME: agora-scorer-worker
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://alloy:4318}
ports:
- "${SCORER_ADMIN_PORT:-4001}:4001"
mem_limit: "512m"
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
scorer-toxicity:
condition: service_started
scorer-relationship:
condition: service_started
neo4j:
condition: service_healthy
init: true
restart: unless-stopped
# ── Caddy front door (rides the data-plane profiles) — routes to the HOST dev code ────────────────
# Same image/Caddyfile as prod, but the upstreams point at host.docker.internal and the SPA catch-all
# is reverse-proxied to the host vite dev server (HMR rides the proxied websocket) instead of the
# baked static build. The dev routing snippet (agora-routes.dev.caddy) is mounted OVER the baked
# /etc/caddy/agora-routes.caddy that the Caddyfile imports.
proxy:
profiles: ["supabase", "selfhost"]
build:
context: .
dockerfile: deploy/proxy/Dockerfile
image: agora-proxy
environment:
# localhost → Caddy's internal CA, so the front door is HTTPS with no setup. Plain HTTP: SERVER_NAME=:80.
SERVER_NAME: ${SERVER_NAME:-localhost}
MAX_BODY_SIZE: ${MAX_BODY_SIZE:-25MB}
ACME_EMAIL: ${ACME_EMAIL:-}
ACME_CA: ${ACME_CA:-https://acme-staging-v02.api.letsencrypt.org/directory}
# App surfaces live on the HOST; moderator/minio/demo stay in-network. All resolved lazily, so a
# route just 502s while its target (a host process you haven't started, an off-profile container) is down.
API_UPSTREAM: ${API_UPSTREAM:-http://host.docker.internal:4000}
SECURE_CHAT_UPSTREAM: ${SECURE_CHAT_UPSTREAM:-http://host.docker.internal:4002}
ADMIN_UPSTREAM: ${ADMIN_UPSTREAM:-http://host.docker.internal:5173}
MODERATOR_UPSTREAM: ${MODERATOR_UPSTREAM:-http://scorer-worker:4001}
MINIO_UPSTREAM: ${MINIO_UPSTREAM:-http://minio:9000}
DEMO_UPSTREAM: ${DEMO_UPSTREAM:-http://demo:80}
# Grafana upstream (the `observability` profile). Front door serves it at /grafana/ (502s until up).
GRAFANA_UPSTREAM: ${GRAFANA_UPSTREAM:-http://grafana:3000}
# GoTrue upstream (selfhost SSO). Lazily resolved — /auth/v1/* 502s until the profile is up.
GOTRUE_UPSTREAM: ${GOTRUE_UPSTREAM:-http://gotrue:9999}
S3_BUCKET: ${S3_BUCKET:-agora}
# Origin of your PUBLIC consumer app, for the admin's "Open in app" deep links. The proxy
# entrypoint writes it into the BAKED SPA's /config.js — but in THIS dev compose the admin is
# served from the host vite server (ADMIN_UPSTREAM above), which serves its own config.js, so
# this is inert here. For host-side dev set VITE_PUBLIC_APP_URL in apps/admin/.env instead.
# It takes effect if you point the SPA route back at the image's baked build.
AGORA_PUBLIC_APP_URL: ${AGORA_PUBLIC_APP_URL:-}
# The remaining admin settings, same runtime seam (all optional — unset keeps the image's
# build-time default). Lets ONE published image serve a different project / API origin / feature
# set with no rebuild.
AGORA_ADMIN_API_BASE_URL: ${AGORA_ADMIN_API_BASE_URL:-}
AGORA_ADMIN_MODERATOR_BASE_URL: ${AGORA_ADMIN_MODERATOR_BASE_URL:-}
AGORA_ADMIN_PROJECT_ID: ${AGORA_ADMIN_PROJECT_ID:-}
AGORA_ADMIN_SOCIAL_GRAPH_ENABLED: ${AGORA_ADMIN_SOCIAL_GRAPH_ENABLED:-}
# UI guard only, NOT a security boundary — the server enforces via OPERATOR_RO_EMAILS.
AGORA_ADMIN_SETTINGS_READ_ONLY: ${AGORA_ADMIN_SETTINGS_READ_ONLY:-}
# Social sign-in buttons on the admin login screen, e.g. "google,github,apple". Each
# provider must also be configured on the gotrue service. Unset → email+password only.
AGORA_ADMIN_OAUTH_PROVIDERS: ${AGORA_ADMIN_OAUTH_PROVIDERS:-}
# ⚠️ PUBLIC: served to every visitor in /config.js. Point at the shared demo login only.
# Optional Umami analytics for the admin app — BOTH required or tracking stays off. Browser-side
# only: the admin posts events straight to your Umami instance; the API has no analytics code.
AGORA_ADMIN_UMAMI_URL: ${AGORA_ADMIN_UMAMI_URL:-}
AGORA_ADMIN_UMAMI_ID: ${AGORA_ADMIN_UMAMI_ID:-}
AGORA_ADMIN_DEMO_EMAIL: ${AGORA_ADMIN_DEMO_EMAIL:-}
AGORA_ADMIN_DEMO_PASSWORD: ${AGORA_ADMIN_DEMO_PASSWORD:-}
volumes:
- ${CADDYFILE:-./deploy/proxy/Caddyfile}:/etc/caddy/Caddyfile:ro
# Dev routing snippet — SPA + /assets reverse-proxied to the host vite server (no immutable cache).
- ./deploy/proxy/agora-routes.dev.caddy:/etc/caddy/agora-routes.caddy:ro
- ${CADDY_CERTS_DIR:-./deploy/proxy/certs}:/certs:ro
- caddy-data:/data
- caddy-config:/config
ports:
- "${HTTP_PORT:-80}:80"
- "${HTTPS_PORT:-443}:443"
# The internal GoTrue shim, published for DEV only: the api runs on the HOST here, so it needs
# a reachable SUPABASE_URL (http://localhost:9998). Container deploys keep it network-internal.
- "${GOTRUE_SHIM_PORT:-9998}:9998"
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
scorer-worker:
condition: service_started
required: false
init: true
restart: unless-stopped
# Scheduler: fires the secret-gated /internal/cron/* endpoints against the HOST API on the crontab
# schedule. Needs CRON_SECRET in .env (without it the endpoints 503).
cron:
profiles: ["supabase", "selfhost"]
build:
context: .
dockerfile: apps/api/Dockerfile.cron
image: agora-cron
env_file: .env
environment:
AGORA_URL: ${AGORA_URL:-http://host.docker.internal:4000}
SECURE_CHAT_URL: ${SECURE_CHAT_URL:-http://host.docker.internal:4002}
# OpenTelemetry — OFF by default (bare deploys stay dark). Pre-wired to `observability` profile's
# Alloy collector. To export: set OTEL_SDK_DISABLED=false in .env + add --profile observability.
OTEL_SDK_DISABLED: ${OTEL_SDK_DISABLED:-true}
OTEL_TRACES_ENDPOINT: ${OTEL_TRACES_ENDPOINT:-http://alloy:4318/v1/traces}
OTEL_METRICS_ENDPOINT: ${OTEL_METRICS_ENDPOINT:-http://alloy:4318/v1/metrics}
OTEL_LOGS_ENDPOINT: ${OTEL_LOGS_ENDPOINT:-http://alloy:4318/v1/logs}
extra_hosts:
- "host.docker.internal:host-gateway"
init: true
restart: unless-stopped
# ── agora-demo (the `demo` profile) — PULLED, served behind the front door at /demo/ ──────────────
demo:
profiles: ["demo"]
image: ${AGORA_DEMO_IMAGE:-docker.io/agoraserver/agora-demo:latest}
environment:
# Browser-facing — same-origin through Caddy → host API. Override for a non-localhost front door.
AGORA_DEMO_API_BASE_URL: ${AGORA_DEMO_API_BASE_URL:-http://localhost/v7}
# See docker-compose.yml's demo service for what each of these does. BASE_PATH must match the
# front door's handle_path (/demo/) or the emailed-link pages render blank; EMAIL_REDIRECT_TO
# must be overridden because the image defaults it to the PUBLIC demo origin.
AGORA_DEMO_PROJECT_ID: ${AGORA_DEMO_PROJECT_ID:-11111111-1111-1111-1111-111111111111}
AGORA_DEMO_BASE_PATH: ${AGORA_DEMO_BASE_PATH:-/demo/}
AGORA_DEMO_EMAIL_REDIRECT_TO: ${AGORA_DEMO_EMAIL_REDIRECT_TO:-http://localhost/demo/}
AGORA_DEMO_GIPHY_API_KEY: ${AGORA_DEMO_GIPHY_API_KEY:-}
init: true
restart: unless-stopped
# ── Observability stack (the `observability` profile) ─────────────────────────────────────────────
# OTLP receiver published on :4317/:4318 so the HOST apps can export when OTEL_SDK_DISABLED=false
# (point their OTEL_*_ENDPOINT at http://localhost:4318).
alloy:
profiles: ["observability"]
image: grafana/alloy:v1.5.1
command:
- run
- /etc/alloy/config.alloy
- --storage.path=/var/lib/alloy/data
- --server.http.listen-addr=0.0.0.0:12345
volumes:
- ./deploy/observability/config.alloy:/etc/alloy/config.alloy:ro
- alloy-data:/var/lib/alloy/data
ports:
- "${ALLOY_PORT:-12345}:12345" # Alloy UI
- "${ALLOY_OTLP_GRPC_PORT:-4317}:4317"
- "${ALLOY_OTLP_HTTP_PORT:-4318}:4318"
depends_on:
- tempo
- mimir
- loki
init: true
restart: unless-stopped
tempo:
profiles: ["observability"]
image: grafana/tempo:2.6.1
command: ["-config.file=/etc/tempo/tempo.yaml"]
volumes:
- ./deploy/observability/tempo.yaml:/etc/tempo/tempo.yaml:ro
- tempo-data:/var/tempo
init: true
restart: unless-stopped
mimir:
profiles: ["observability"]
image: grafana/mimir:2.14.2
command: ["-config.file=/etc/mimir/mimir.yaml"]
volumes:
- ./deploy/observability/mimir.yaml:/etc/mimir/mimir.yaml:ro
- mimir-data:/data
init: true
restart: unless-stopped
loki:
profiles: ["observability"]
image: grafana/loki:3.3.2
command: ["-config.file=/etc/loki/loki-config.yaml"]
volumes:
- ./deploy/observability/loki-config.yaml:/etc/loki/loki-config.yaml:ro
- loki-data:/loki
init: true
restart: unless-stopped
grafana:
profiles: ["observability"]
image: grafana/grafana:11.4.0
environment:
GF_AUTH_ANONYMOUS_ENABLED: "true"
GF_AUTH_ANONYMOUS_ORG_ROLE: Admin
GF_AUTH_DISABLE_LOGIN_FORM: "true"
GF_FEATURE_TOGGLES_ENABLE: traceqlEditor
# Served behind the Caddy front door at /grafana/ (as well as the direct :3000 below). serve-from-
# sub-path needs the matching root_url so Grafana's asset + redirect URLs carry the prefix.
GF_SERVER_ROOT_URL: ${GRAFANA_ROOT_URL:-https://localhost/grafana/}
GF_SERVER_SERVE_FROM_SUB_PATH: "true"
volumes:
- ./deploy/observability/grafana/provisioning:/etc/grafana/provisioning:ro
- ./deploy/observability/grafana/dashboards:/var/lib/grafana/dashboards:ro
- grafana-data:/var/lib/grafana
ports:
- "${GRAFANA_PORT:-3000}:3000"
depends_on:
- tempo
- mimir
- loki
init: true
restart: unless-stopped
volumes:
neo4j-data:
caddy-data:
caddy-config:
scorer-hf-cache:
db-data:
minio-data:
alloy-data:
tempo-data:
mimir-data:
loki-data:
grafana-data: