-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
373 lines (359 loc) · 17 KB
/
Copy pathdocker-compose.yml
File metadata and controls
373 lines (359 loc) · 17 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
# Comment touch (#974/#981 fix-PR trigger): forcing this PR through the existing
# docker-images.yml `paths` filter so the workflow fires on it. After Phase A
# lands, future PRs trigger the workflow regardless of paths touched.
# Continuum — docker compose up
#
# FIRST-TIME SETUP (fresh clone): populate vendored substrates before build.
# git submodule update --init --recursive
# The Dockerfiles fail fast with a clear message if you skip this step.
#
# Local: docker compose up (HTTP on localhost, live calls on ws://localhost:7880)
# Grid: docker compose --profile grid up (HTTPS via Tailscale, WebRTC over Tailscale mesh)
# GPU: docker compose --profile gpu up (adds forge + inference)
# All: docker compose --profile grid --profile gpu up
#
# Grid mode .env (required):
# TS_AUTHKEY=tskey-auth-...
# TS_HOSTNAME=my-machine
# LIVEKIT_HOST_PORT=0 # disable local livekit host binding (grid uses livekit-grid)
# LIVEKIT_URL=ws://tailscale:7890 # continuum-core connects to livekit-grid internally
# LIVEKIT_BROWSER_URL=ws://tailscale:7880 # node-server returns port 7880 to browser (Tailscale serve TLS proxy)
services:
# ── PostgreSQL (OPT-IN, grid deployments only) ────────────
# Default install uses SQLite at ~/.continuum/database/main.db — zero-dep,
# portable, works offline. Postgres is only needed for multi-writer grid
# setups where multiple continuum-core nodes share state over Tailscale.
#
# Enable with: docker compose --profile postgres up
# And tell Rust to use it: DATABASE_URL=postgres://continuum:continuum@postgres:5432/continuum
#
# When this profile is inactive, `depends_on: postgres` entries on other
# services are auto-skipped by compose — postgres simply doesn't run.
postgres:
profiles: [postgres]
image: postgres:17-alpine
restart: unless-stopped
mem_limit: 512m
environment:
POSTGRES_DB: continuum
POSTGRES_USER: ${POSTGRES_USER:-continuum}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-continuum}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U continuum"]
interval: 5s
timeout: 3s
retries: 5
# ── Model Init (downloads voice/avatar/scene models on first run) ──
model-init:
build:
context: ./src
dockerfile: ../docker/model-init.Dockerfile
image: ghcr.io/cambriantech/continuum-model-init:${CONTINUUM_IMAGE_TAG:-latest}
# One-time downloader. Fixed budget — doesn't scale with host RAM.
mem_limit: ${MODEL_INIT_MEM:-2g}
volumes:
- voice-models:/models
environment:
- MODELS_DIR=/models
- HF_TOKEN=${HF_TOKEN:-}
# base: small (74MB), fast, works with all whisper.cpp versions.
# large-v3-turbo requires whisper.cpp v1.7+ (different tensor count).
- WHISPER_MODEL=${WHISPER_MODEL:-base}
# ── Continuum Core (Rust) ─────────────────────────────────
# Default uses the vulkan variant: software rendering via mesa's llvmpipe ICD
# when no GPU hardware is present, real driver ICD (NVIDIA/Intel/AMD) when one
# is. Joel's 2026-04-23 architectural rule: "lack of GPU integration is
# forbidden". The previous CPU-only 'core' variant violated that by panicking
# on no-GPU per gpu/memory_manager.rs:757. Vulkan-with-llvmpipe satisfies the
# rule (binary exercises the GPU API loader; llvmpipe answers the queries via
# software rasterizer). Removed in #1038 (Task #98) — see
# docs/INSTALL-ARCHITECTURE.md.
#
# CUDA hosts overlay docker-compose.gpu.yml to swap in continuum-core-cuda for
# NVIDIA-accelerated inference. Mac runs continuum-core natively (overlay
# docker-compose.mac.yml sets replicas:0 here).
continuum-core:
build:
context: ./core
dockerfile: ../../docker/continuum-core-vulkan.Dockerfile
additional_contexts:
# NOTE: the `avatars: ./src/models/avatars` line was here from
# 9b1f6ca2a "Bake CC0 avatar VRM models into continuum-core image"
# (April 2026), but src/models is gitignored — the directory
# doesn't exist in CI checkouts and the build context fails to
# resolve, breaking carl-install-smoke for any PR that touches
# install.sh (e.g. #1475). The Dockerfile already handles the
# empty-dir case via `RUN mkdir -p /app/avatars` (see
# docker/continuum-core.Dockerfile line 143 and the explanatory
# comment block at lines 131-142). No Dockerfile uses
# `--from=avatars`, so the context declaration was dangling
# (referenced nowhere, broke everywhere). Restore when the
# avatar-provisioning story lands (LFS, model-init download,
# or curl from a CC0 URL in CI before docker build) per the
# gap noted in PR891-E2E-VALIDATION.md.
shared: ./src/shared
shared-generated: ./src/shared/generated
args:
# --no-default-features excludes livekit-webrtc (handled by livekit-bridge).
# load-dynamic-ort loads ONNX Runtime as shared lib (runtime discovery).
# vulkan feature wires through to llama.cpp's GGML_VULKAN backend.
GPU_FEATURES: "--no-default-features --features load-dynamic-ort,vulkan"
image: ghcr.io/cambriantech/continuum-core-vulkan:${CONTINUUM_IMAGE_TAG:-latest}
restart: unless-stopped
# Sized for mission: Qwen 4-8B Q4 + KV cache for 5 personas + embeddings
# + Bevy render + vision + audio. Auto-calculated by install.sh from host
# RAM (host_gb - 10 on ≥32GB, host_gb - 8 on 20-31GB, floor 10g). On Mac
# this service runs NATIVE (docker-compose.mac.yml sets replicas=0), so
# this limit is unused there — informational. On Linux (continuum-core-
# cuda / continuum-core-vulkan overlays) it's the actual ceiling.
mem_limit: ${CONTINUUM_CORE_MEM:-16g}
working_dir: /app
# No depends_on for services behind profiles (postgres, livekit-bridge).
# Core starts independently; connections to optional services (postgres
# pool, livekit bridge socket) retry on demand. Text chat works without
# any profile active — voice/video requires `--profile live`.
volumes:
- voice-models:/app/models:ro
# Mount the ENTIRE ~/.continuum directory R/W. The Rust core reads config,
# writes model cache, logs, grid state, sockets, sessions — all under
# ~/.continuum. Cherry-picking subdirs with :ro caused silent failures
# whenever the core needed to write to a path that wasn't mounted.
- ~/.continuum:/root/.continuum
# GPU access for Bevy headless 3D rendering (avatar snapshots, live video).
# NOTE: the `runtime:` key is Docker-only — Podman (Rancher alt) doesn't
# know it and errors out. NVIDIA runtime is now set ONLY in the gpu
# overlay (docker-compose.gpu.yml), not here in the base file, so this
# base file stays Podman/Rancher-compatible. Default CPU-fallback for
# Bevy avatar rendering happens automatically when no runtime is set.
environment:
# DATABASE_URL is OPTIONAL. Unset → Rust defaults to SQLite at
# ~/.continuum/database/main.db (mounted from host via the
# ~/.continuum volume). Set to `postgres://continuum:continuum@postgres:5432/continuum`
# only when running the `postgres` compose profile. Rust's
# resolve_handle('main') reads this env to pick the adapter.
- DATABASE_URL=${DATABASE_URL:-}
- AVATAR_MODELS_DIR=/app/avatars
- LIVEKIT_URL=${LIVEKIT_URL:-ws://livekit:7880}
- NVIDIA_VISIBLE_DEVICES=${NVIDIA_VISIBLE_DEVICES:-all}
- NVIDIA_DRIVER_CAPABILITIES=compute,graphics,utility
# HuggingFace cache inside persistent ~/.continuum so model downloads survive restarts
- HF_HOME=/root/.continuum/hf_cache
healthcheck:
# Verify IPC responds, not just socket exists. Uses archive-worker binary
# (already in image) as a lightweight ping — connects, sends ping, checks response.
# Falls back to socket file check if binary missing.
test: ["CMD-SHELL", "test -S /root/.continuum/sockets/continuum-core.sock"]
interval: 10s
timeout: 5s
start_period: 30s
retries: 3
# ── LiveKit Bridge (Rust — WebRTC transport adapter) ──────
# Links webrtc-sys but NOT ort. Separate process eliminates
# the protobuf symbol conflict that deadlocked continuum-core.
#
# Behind `live` profile: voice/video chat is opt-in. Text chat (the
# default first-chat experience) doesn't need LiveKit at all. This
# saves ~300MB RAM + 3 ports (7880-7882) for Carl's first run.
# Enable with: docker compose --profile live up
livekit-bridge:
profiles: [live]
build:
context: ./core
dockerfile: ../../docker/livekit-bridge.Dockerfile
image: ghcr.io/cambriantech/continuum-livekit-bridge:${CONTINUUM_IMAGE_TAG:-latest}
restart: unless-stopped
mem_limit: ${LIVEKIT_BRIDGE_MEM:-2g}
depends_on:
- livekit
volumes:
- ~/.continuum/sockets:/root/.continuum/sockets
environment:
- LIVEKIT_URL=${LIVEKIT_URL:-ws://livekit:7880}
healthcheck:
test: ["CMD", "test", "-S", "/root/.continuum/sockets/livekit-bridge.sock"]
interval: 5s
timeout: 3s
retries: 5
# ── Node Server (TypeScript) ──────────────────────────────
node-server:
build:
context: ./src
dockerfile: ../docker/node-server.Dockerfile
image: ghcr.io/cambriantech/continuum-node:${CONTINUUM_IMAGE_TAG:-latest}
restart: unless-stopped
# TS orchestrator + IPC buffers + RAG state. Scales with host RAM —
# install.sh sets NODE_SERVER_MEM to max(2, host_gb/8). Default 2g
# for manual docker compose users.
mem_limit: ${NODE_SERVER_MEM:-2g}
# depends_on omits postgres — it's in the `postgres` profile (opt-in).
# node-server doesn't connect to postgres directly anyway; all data ops
# flow through continuum-core via IPC with opaque handles.
depends_on:
continuum-core:
condition: service_healthy
ports:
- "${NODE_WS_PORT:-9001}:9001" # WebSocket
volumes:
- ~/.continuum:/root/.continuum
environment:
# node-server never directly connects to a database — all data ops
# go through continuum-core via IPC, using opaque handles ('main' for
# the primary DB, persona UUIDs for per-persona DBs). Rust resolves
# handles to concrete backends. DATABASE_URL intentionally NOT set
# here: the old value leaked across the TS→Rust IPC boundary as a
# connection string and broke Mac Option B where docker-DNS doesn't
# resolve from the native host.
- NODE_ENV=production
- JTAG_SKIP_HTTP=1
- JTAG_NO_TLS=1
# Browser connects to LiveKit via host-mapped port, not Docker DNS.
# 'ws://livekit:7880' only resolves inside the Docker network;
# the browser runs on the host where 'livekit' doesn't resolve.
# localhost:7880 works because livekit binds that port to the host.
# Grid mode overrides via LIVEKIT_BROWSER_URL=ws://tailscale:7880.
- LIVEKIT_URL=${LIVEKIT_BROWSER_URL:-ws://localhost:7880}
# ── Widget Server (Vite) ──────────────────────────────────
widget-server:
build:
context: ./src
dockerfile: ../docker/widget-server.Dockerfile
image: ghcr.io/cambriantech/continuum-widgets:${CONTINUUM_IMAGE_TAG:-latest}
restart: unless-stopped
mem_limit: 512m
depends_on:
node-server:
condition: service_healthy
ports:
- "9003:9003" # HTTP
volumes:
- ~/.continuum/config.env:/root/.continuum/config.env:ro
environment:
- JTAG_HTTP_PORT=9003
- JTAG_WEBSOCKET_PORT=${NODE_WS_PORT:-9001}
# WS proxy: widget-server forwards browser WebSocket to node-server container
- JTAG_WS_PROXY_HOST=node-server
- JTAG_WS_PROXY_PORT=9001
# ── LiveKit (WebRTC) — local mode ───────────────────────────
# Dev server for voice/video. Behind `live` profile — text chat doesn't
# need it. In grid mode, set LIVEKIT_HOST_PORT=0 to avoid port conflict.
# Enable with: docker compose --profile live up
livekit:
profiles: [live]
image: livekit/livekit-server:latest
restart: unless-stopped
mem_limit: 256m
ports:
- "${LIVEKIT_HOST_PORT:-7880}:7880" # WebSocket signalling
- "${LIVEKIT_HOST_RTC_TCP:-7881}:7881" # WebRTC TCP
- "${LIVEKIT_HOST_RTC_UDP:-7882}:7882/udp" # WebRTC UDP
command: ["--dev", "--bind", "0.0.0.0", "--node-ip", "127.0.0.1"]
# ── LiveKit (WebRTC) — grid mode ──────────────────────────
# Shares Tailscale's network namespace so WebRTC media ports (7881/7882)
# are directly on the Tailscale interface — real UDP, no relay, no TURN.
# Signaling proxied by Tailscale serve: wss://7880 → localhost:7890.
livekit-grid:
image: livekit/livekit-server:latest
restart: unless-stopped
mem_limit: 256m
profiles: ["grid"]
network_mode: "service:tailscale"
depends_on:
- tailscale
volumes:
- ./docker/livekit.yaml:/etc/livekit.yaml:ro
- ./docker/livekit-entrypoint.sh:/entrypoint.sh:ro
entrypoint: ["/entrypoint.sh"]
# ── Tailscale (grid profile — remote HTTPS access) ────────
# Joins tailnet, provisions real Let's Encrypt certs, reverse-proxies
# HTTPS/WSS to plain HTTP containers. Only needed for remote access.
#
# TS_AUTHKEY from ~/.continuum/config.env (same as all other API keys).
# Once registered, state volume persists identity — key only needed for first run.
tailscale:
image: tailscale/tailscale:latest
hostname: ${TS_HOSTNAME:-${HOSTNAME}-grid}
restart: unless-stopped
profiles: ["grid"]
environment:
- TS_AUTHKEY=${TS_AUTHKEY:-}
- TS_STATE_DIR=/var/lib/tailscale
- TS_SERVE_CONFIG=/config/tailscale-serve.json
- TS_USERSPACE=false
volumes:
- tailscale-state:/var/lib/tailscale
- ./docker/tailscale-serve.json:/config/tailscale-serve.json:ro
devices:
- /dev/net/tun:/dev/net/tun
cap_add:
- net_admin
- net_raw
ports:
- "443:443" # HTTPS → widget-server (Tailscale serve proxies to widget-server:9003)
# LiveKit 7880 NOT mapped here — local livekit already binds it on the host.
# livekit-grid is in Tailscale's network namespace, reachable via Tailscale IP directly.
# ── Forge Worker (sentinel-ai) ────────────────────────────
forge-worker:
build: ../sentinel-ai
# forge-worker is built and published by the sibling sentinel-ai repo
# (https://github.com/CambrianTech/sentinel-ai), which has its own release
# cadence independent of continuum's PR cycle. It does NOT get tagged with
# continuum's :pr-<N> or :<sha> — its tags are :latest + commit-shas of
# sentinel-ai pushes only. Coupling this to CONTINUUM_IMAGE_TAG made
# `docker compose --profile gpu pull` on a continuum PR tag fail with
# "manifest unknown" (caught 2026-04-23 during PR #950 Carl-GPU testing).
# Uses FORGE_WORKER_IMAGE_TAG (default :latest) so the two repos stay
# independently versioned.
image: ghcr.io/cambriantech/forge-worker:${FORGE_WORKER_IMAGE_TAG:-latest}
profiles: ["gpu"]
mem_limit: 28g
deploy:
resources:
limits:
memory: 28g
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
volumes:
- forge-output:/app/output
- hf-cache:/root/.cache/huggingface
- models:/app/models
- ../forge-alloy/examples:/app/alloys:ro
environment:
- CUDA_VISIBLE_DEVICES=0
- HF_TOKEN=${HF_TOKEN:-}
- PYTHONUNBUFFERED=1
# ── Inference Server (GPU nodes only) ──────────────────────
inference:
# Pinned to a specific upstream digest. The floating `:server-cuda` tag is
# rebuilt by ggml-org on every merge to llama.cpp main; if Carl pulls on a
# day when upstream rolls a breaking change, every install silently breaks
# with no signal pointing at the cause and no way for us to reproduce. Pin
# forces deliberate updates where we verify behavior parity first. Bump
# the digest in a follow-up PR after smoke-testing the new upstream build.
# Issue #955.
image: ghcr.io/ggml-org/llama.cpp:server-cuda@sha256:11b71618f3f4b9c98e42818c058e37b62478f474806b4107ab698abd0be900f6
restart: unless-stopped
profiles: ["gpu"]
mem_limit: 8g
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
volumes:
- models:/models
- forge-output:/app/output:ro
command: ["-m", "/models/current.gguf", "-c", "4096", "-ngl", "99", "--port", "8090", "--host", "0.0.0.0"]
volumes:
pgdata:
hf-cache:
forge-output:
models:
voice-models:
tailscale-state: