Repository navigation
Expand file tree
/
Copy pathcompose.yaml
More file actions
364 lines (350 loc) · 14.3 KB
/
Copy pathcompose.yaml
File metadata and controls
364 lines (350 loc) · 14.3 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
# 2dph — docker composition
#
# scripts/stack/start / start-assistant / status / stop
# docker compose up -d brain # API (Zig CGO serve)
# docker compose --profile index run --rm index # Go rebuild (zig cgo)
# docker compose --profile index up -d index-sync # periodic gator→graph→index (#292)
# docker compose --profile picoclaw up -d # brain-mcp + CPU reasoner + PicoClaw gateway
# docker compose --profile reasoner up -d reasoner # CPU Ollama :11435
# docker compose --profile searxng up -d
# OCR_ENGINE=paddle docker compose --profile ocr-paddle run --rm ocr-paddle
#
# Secrets never baked in: search.env + db-profiles.yml from ~/.config/brain.
name: 2dph
networks:
default:
name: 2dph_sys
driver: bridge
ipam:
config:
- subnet: 10.23.42.0/24
services:
brain:
image: ghcr.io/eslider/2dph:api
build:
context: .
dockerfile: Dockerfile
target: api
cache_from:
- ghcr.io/eslider/2dph:cache
command: ["serve"]
# Run as the host user, not the image's uid 1001, so files written into
# ./var match the host tools that open the same kb.lbug (#195). Override
# with KB_UID/KB_GID on hosts with a different account.
user: "${KB_UID:-1000}:${KB_GID:-1000}"
environment: &env
HOME: /data
HF_HOME: /data/var/hf
KB_HOST: 0.0.0.0
BRAIN_SEARCH_CACHE: /data/var/cache/web-search.sqlite
BRAIN_DB_PROFILES: /secret/db-profiles.yml
BRAIN_SEARCH_ENV: /secret/search.env
KB_ROOT: /data
KB_WORKERS: "4"
KB_PORT: "8630"
# Ladybug buffer pool. The served DB is ~11 MB (#335), so the old 4 GiB
# pool only wasted address space/RSS. Rebuild (index profile) keeps its
# own larger pool; the serve/incremental pool stays at 512 MiB — ample
# for the DB size while still leaving headroom for concurrent searches.
KB_BUFFER_POOL: "536870912"
# In-process embedding: the daemon on KBSEARCH_PORT is only started by
# the entrypoint when the image carries a working brain-search; embedding
# in-process avoids a respawn loop against a dead daemon. The model is
# loaded once per process into a shared cache (#335) and reused for every
# /search — it is no longer reloaded per request.
KBSEARCH_NO_DAEMON: "1"
# pprof (heap/cpu) on loopback only; the API host stays 0.0.0.0.
KB_PPROF: "1707"
network_mode: host
volumes:
- ./var:/data/var
# Repo-root config stack (etc/brain/config.yml + config.local.yml):
# config.Load resolves it against /data (KB_ROOT, WORKDIR /data), so it
# must be mounted — the image carries no etc/ (#208). Secrets stay in
# ~/.config/brain (/secret).
- ./etc/brain:/data/etc/brain:ro
- ~/.config/brain:/secret:ro
read_only: true
tmpfs:
- /tmp
# Probe the DB read path, not the static /health: a wedged brain still
# answers /health "ok" while every search/get/audit hangs on the read
# lock. /stats runs a real MATCH over kb.lbug, so when the read handle is
# deadlocked it times out here and docker (restart: unless-stopped) recycles
# the container instead of leaving it "healthy" and unresponsive (#180).
healthcheck:
test: ["CMD", "wget", "-qO-", "--timeout=3", "http://127.0.0.1:8630/stats"]
interval: 30s
timeout: 5s
retries: 3
restart: unless-stopped
stop_grace_period: 20s
# Hard memory ceiling so a regression cannot eat host swap again (#335:
# the container was unbounded — HostConfig.Memory=0 — and the per-request
# model reload pushed 11 GiB into swap). 2 GiB covers the ~0.5 GiB shared
# model + ANN snapshot + Ladybug pool with headroom.
deploy:
resources:
limits:
memory: 2g
brain-watch:
image: ghcr.io/eslider/2dph:api
environment: *env
volumes:
- ./var:/data/var
- ./etc/brain:/data/etc/brain:ro
- ..:/corpus:ro
- ~/.config/brain:/secret:ro
command: ["watch", "/corpus"]
read_only: true
tmpfs:
- /tmp
restart: unless-stopped
stop_grace_period: 20s
# Go write path (Ladybug full rebuild, brain-index Zig binary). Same api image.
# docker compose --profile index run --rm index --rebuild --with-mail
# For the periodic incremental cycle (gator graph + index) use index-sync below.
index:
profiles: ["index"]
image: ghcr.io/eslider/2dph:api
build:
context: .
dockerfile: Dockerfile
target: api
environment:
<<: *env
# sanctioned swap+restart flow: the live-holder guard must not block it
KB_INDEX_ALLOW_LIVE: "1"
volumes:
- ./var:/data/var
- ./etc/brain:/data/etc/brain:ro
- ..:/corpus:ro
- ~/.config/brain:/secret:ro
command: ["index"]
read_only: true
tmpfs:
- /tmp
# Periodic cycle that closes the chain gator sync→etl→pack → 2dph
# import(graph) → index(kb.lbug) (issue #292). It is the ONLY automatic
# writer: every interval it stops the `brain` container (Ladybug is
# single-writer), imports the document tree, indexes the mail canon, then
# starts brain again. Mail canon (T10-A) is selected by MAIL_SOURCE:
# corpus (default) — brain-index --with-mail indexes var/corpus/mail/m365
# (the local 2dph M365 corpus); the gator parquet/mail
# leaf+graph path is skipped (single canon).
# gator — mail-leaf + mail-graph over the gator parquet/mail hive.
# A cycle is skipped when kb.lbug is already newer than the newest upstream
# state, so the brain is not bounced for nothing. When ANN_BIN is set, each
# cycle ends with `<ANN_BIN> ensure`, keeping the IVF vector index current
# (var/state/vector.ann) so /stats shows ann.loaded=true. systemd is not used.
#
# docker compose --profile index up -d index-sync
# docker compose --profile index run --rm index-sync index-loop --once # manual control
#
# Host gator var path is machine-local: GATOR_VAR_HOST in .env (gitignored).
# The docker socket is required to stop/start the sibling `brain` container.
index-sync:
profiles: ["index"]
image: ghcr.io/eslider/2dph:api
build:
context: .
dockerfile: Dockerfile
target: api
# Same host-uid alignment as brain/mail-sync (#195): kb.lbug must stay
# writable by the host tools that open it.
user: "${KB_UID:-1000}:${KB_GID:-1000}"
# A container started with an explicit `user:` does not inherit the host
# user's supplementary groups, so the mounted /var/run/docker.sock (group
# `docker`, mode 660) is not accessible and the quiesce step fails with
# "connect: permission denied". Add the socket's group explicitly.
# Override DOCKER_GID on hosts where it differs from 991:
# stat -c '%g' /var/run/docker.sock
group_add:
- "${DOCKER_GID:-991}"
environment:
<<: *env
# The cycle quiesces brain itself; allow the sanctioned swap+restart
# flow (compose `index`) to run its rebuild guard too.
KB_INDEX_ALLOW_LIVE: "1"
# T10-A mail canon: the local 2dph M365 corpus under ./var/corpus/mail
# (mounted at /data/var/corpus/mail). gator mail leaf/graph is skipped.
MAIL_SOURCE: "corpus"
# ANN index (vector.ann): index-loop reads ANN_BIN and runs
# `<ANN_BIN> ensure` at the end of every cycle after indexing (full build
# when missing/stale, incremental WAL-upsert otherwise). The binary is
# built into the api image; the index lands in /data/var/state/vector.ann
# (= the ./var mount) so it persists across cycles/restarts. Empty =
# skip and vector search falls back to the linear scan.
ANN_BIN: "/usr/local/bin/brain-ann"
# Hive root INSIDE the container (the host path is GATOR_VAR_HOST below).
GATOR_MAIL_HIVE: "/gator/parquet/mail"
# Same mount covers parquet/documents; empty/absent tree = doc step skipped.
GATOR_DOCUMENTS_HIVE: "/gator/parquet/documents"
DOCKER_HOST: "unix:///var/run/docker.sock"
# External skills corpus (#317): index-loop passes --corpus /skills to
# brain-index so principles/how-to stay current in the brain. Default
# ./skills exists in the repo; override the host path in .env
# (AGENT_SKILLS_DIR=<host skills dir>).
BRAIN_CORPUS: "/skills"
volumes:
- ./var:/data/var
- ./etc/brain:/data/etc/brain:ro
- ~/.config/brain:/secret:ro
# gator canon (read-only). Set GATOR_VAR_HOST=<gator repo>/var/gator in
# .env; the default keeps `docker compose config` valid on a fresh clone.
- ${GATOR_VAR_HOST:-./var/gator}:/gator:ro
# Agent skills corpus, read-only (#317). Host path overridable in .env.
- ${AGENT_SKILLS_DIR:-./skills}:/skills:ro
- /var/run/docker.sock:/var/run/docker.sock
command: ["index-loop", "--interval", "1h"]
depends_on:
- brain
# The image HEALTHCHECK probes brain's :8630/health; this service serves
# nothing on that port, so without disabling it the loop is perpetually
# "unhealthy" (failing wget) even while cycling correctly.
healthcheck:
disable: true
read_only: true
tmpfs:
- /tmp
restart: unless-stopped
stop_grace_period: 60s
# mail-sync ETL: download (existing mailsync binary) → bounded runner
# pipeline import (#98) every 300s into shared var, with a per-run stats YAML.
# Full brain rebuild only when MAIL_SYNC_INDEX=1 (expensive on large mail
# corpora). Default sources: onlyoffice,gmail. For M365 set MAIL_SYNC_SRC=m365
# and put M365_TENANT/CLIENT_ID/CLIENT_SECRET/USERS in ~/.config/brain/mail.env
# (or m365.env + MAIL_SYNC_ENV). Gmail OAuth: mount ~/.gmail-mcp.
# docker compose up -d mail-sync
# scripts/stack/start-mail-sync
mail-sync:
image: ghcr.io/eslider/2dph:api
build:
context: .
dockerfile: Dockerfile
target: api
# Same host-uid alignment as brain (#195): the ETL loop writes var/corpus
# as the host user so host tools can read it.
user: "${KB_UID:-1000}:${KB_GID:-1000}"
environment:
HOME: /home/2dph
HF_HOME: /data/var/hf
MAIL_SYNC_SRC: onlyoffice,gmail
MAIL_SYNC_ENV: /secret/mail.env
MAIL_SYNC_INDEX: "0"
volumes:
- ./var:/data/var
- ./etc/brain:/data/etc/brain:ro
- ~/.config/brain:/secret:ro
- ~/.gmail-mcp:/home/2dph/.gmail-mcp:ro
command: ["runner", "300"]
read_only: true
tmpfs:
- /tmp
restart: unless-stopped
stop_grace_period: 20s
# Optional local SearXNG (D3). Skip if BRAIN_SEARCH_URL already points at a
# live instance — do not run a second copy on that host.
# SEARXNG_SECRET=$(openssl rand -hex 32) docker compose --profile searxng up -d
searxng:
profiles: ["searxng"]
image: docker.io/searxng/searxng:2026.8.10-0a118066d
ports:
- "127.0.0.1:${SEARXNG_PORT:-8888}:8080"
environment:
SEARXNG_SECRET: ${SEARXNG_SECRET:-}
volumes:
- ./etc/searxng/settings.yml:/etc/searxng/settings.yml:ro
- ./etc/searxng/limiter.toml:/etc/searxng/limiter.toml:ro
restart: unless-stopped
# MCP endpoint for PicoClaw (and any MCP client).
# docker compose --profile picoclaw up -d
brain-mcp:
profiles: ["picoclaw"]
image: ghcr.io/eslider/2dph:api
environment: *env
volumes:
- ./var:/data/var
- ./etc/brain:/data/etc/brain:ro
- ~/.config/brain:/secret:ro
command: ["serve"]
ports:
- "127.0.0.1:8630:8630"
read_only: true
tmpfs:
- /tmp
restart: unless-stopped
# CPU OpenAI-compatible sidecar (D18). Weights are pulled at runtime, not
# baked into the 2dph image. Does not touch host Ollama on :11434.
# docker compose --profile reasoner up -d reasoner
# docker compose --profile reasoner exec reasoner ollama pull qwen3.5:9b
reasoner:
profiles: ["reasoner", "picoclaw"]
image: docker.io/ollama/ollama:latest
environment:
OLLAMA_NUM_GPU: "0"
OLLAMA_HOST: "0.0.0.0:11434"
ports:
- "127.0.0.1:11435:11434"
volumes:
- reasoner-ollama:/root/.ollama
restart: unless-stopped
# Official PicoClaw gateway. Config has no secrets (Ollama + HTTP MCP).
# Host network: brain/reasoner bind 127.0.0.1 only, so host.docker.internal
# (docker0) cannot reach them. Gateway 127.0.0.1:18790 (not the 18800 launcher).
# If :8630/:11435 are already bound, do not start brain-mcp/reasoner:
# docker compose --profile picoclaw up -d --no-deps picoclaw
picoclaw:
profiles: ["picoclaw"]
image: docker.io/sipeed/picoclaw:v0.3.1
network_mode: host
depends_on:
- brain-mcp
- reasoner
environment:
PICOCLAW_GATEWAY_HOST: "127.0.0.1"
entrypoint: ["picoclaw", "gateway"]
volumes:
- picoclaw-home:/root/.picoclaw
- ./etc/picoclaw/config.json:/root/.picoclaw/config.json:ro
restart: unless-stopped
# Optional PP-OCRv5 (not default). Default OCR is tesseract eng+deu.
# OCR_ENGINE=paddle docker compose --profile ocr-paddle run --rm ocr-paddle
ocr-paddle:
profiles: ["ocr-paddle"]
image: python:3.12-slim
environment:
OCR_ENGINE: paddle
command: ["python", "-c", "print('OCR_ENGINE=paddle; install paddleocr on PATH')"]
# liteparse as a long-lived document→structured-JSON service (epic #219).
# Start once, then call per document via `docker compose exec liteparse
# lit parse ...` — docker exec on a warm container is ~40ms vs ~0.7s for
# `docker run` per document (see docs/research-liteparse.md). The official
# image ships LibreOffice + tesseract + pdfium, so docx/xlsx/pptx and scans
# convert in-place. Entrypoint runs a warmup pass (libs into page cache).
# docker compose up -d liteparse
# docker compose exec liteparse lit parse /v/<rel>.pdf --format json -o /data/work/<name>.json
liteparse:
image: ghcr.io/eslider/2dph:liteparse
build:
context: .
dockerfile: Dockerfile.liteparse
cache_from:
- ghcr.io/eslider/2dph:liteparse-cache
environment:
PDFIUM_LIB_PATH: /usr/local/lib/pdfium-rs/chromium_7897/pdfium-linux-x64/lib
LD_LIBRARY_PATH: /usr/local/lib/pdfium-rs/chromium_7897/pdfium-linux-x64/lib
volumes:
- ./var:/v
- ./var/research/samples:/s:ro
- /tmp:/tmp:rw
entrypoint: ["/entrypoint.sh"]
healthcheck:
test: ["CMD", "lit", "--version"]
interval: 30s
timeout: 5s
retries: 3
restart: unless-stopped
volumes:
reasoner-ollama:
picoclaw-home: