Repository navigation
Expand file tree
/
Copy pathdocker-compose.production.yml
More file actions
298 lines (287 loc) · 12.9 KB
/
Copy pathdocker-compose.production.yml
File metadata and controls
298 lines (287 loc) · 12.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
services:
api:
image: ci/api
volumes:
- secrets-api:/run/secrets:ro
# ONE bind mount holding the importer's three working areas — uploads/
# (staged uploads, TTL'd), inbox/ (the user's drop-in folder), and
# scratch/ (per-run working trees). Sharing a filesystem is what lets a
# staged source move into a run's scratch as an atomic rename instead of
# a copy of a 50GB tree. Bind rather than named volume so a person can
# actually drop export archives into ./import-work/inbox/ on the host.
# The API creates inbox/ world-writable with the sticky bit (like /tmp)
# — on purpose, since the container runs as root and a root-owned 755
# folder would refuse the desktop user's drag-in; it only ever holds
# copies the importer consumes. uploads/ stays root-only: nothing lands
# there except through the API. Operators who put the inbox elsewhere (a
# NAS) can split the mounts; the importer falls back to copy-then-delete
# across filesystems.
- ${CI_IMPORT_WORK_DIR:-./import-work}:/var/lib/ci-import
# The user's own archives, read-only, as upstream ci-import-tools mounted
# them: importable in place via the path tools, never modified, never
# deleted. Point CI_REPO_DIR somewhere else to expose a different folder.
- ${CI_REPO_DIR:-~/Downloads}:/repo:ro
environment:
# This container is the web half; the import worker below consumes the
# heavy queues. Remove the worker service and set this to `all` to run
# single-process.
CI_ROLE: web
ENABLE_HBPE: ${ENABLE_HBPE:-true}
# Off /var/tmp deliberately, and load-bearing: scratch must share the
# inbox's filesystem for the rename above, and living on the mount means
# an interrupted run's working files survive a container recreate, which
# is what the resume sweep restarts from.
CI_IMPORT_SCRATCH_DIR: ${CI_IMPORT_SCRATCH_DIR:-/var/lib/ci-import/scratch}
# JSON array, not a colon list. /repo is the read-only mount above.
CI_IMPORT_ROOTS: ${CI_IMPORT_ROOTS:-["/repo"]}
# Where inbox/ from the mount above lives on the HOST. The API only
# knows /var/lib/ci-import/inbox, a path inside this container, and that
# is what the import page and `ci-import inbox` would tell a person to
# drop files into. Display only — nothing resolves it. Defaults to the
# mount source; set it explicitly when CI_IMPORT_WORK_DIR is relative and
# you want the page to show an absolute path.
CI_IMPORT_INBOX_HOST_DIR: ${CI_IMPORT_INBOX_HOST_DIR:-${CI_IMPORT_WORK_DIR:-./import-work}/inbox}
# OpenAI-compatible chat backend (Hub-injected on managed deploys). When
# unset, the API falls back to native Ollama on OLLAMA_HOST.
LLM_API_BASE: ${LLM_API_BASE:-}
LLM_API_KEY: ${LLM_API_KEY:-}
LLM_DEFAULT_CHAT_MODEL: ${LLM_DEFAULT_CHAT_MODEL:-}
# The small background model (extraction, classification) and the chat
# context window, as the Hub manifest hands them out; unset means the
# compiled-in default and the planner's measured window.
LLM_UTILITY_MODEL: ${LLM_UTILITY_MODEL:-}
CI_LLM_NUM_CTX: ${CI_LLM_NUM_CTX:-}
# Where embeddings run. Set, LLM_EMBEDDING_API_BASE pins the endpoint (any
# engine, an OpenAI-style base or a bare host; OLLAMA_EMBED_HOST is its
# deprecated name and still read); unset, the embedder follows the chat
# backend when that backend serves it (Lemonade, llama.cpp with the model
# loaded) and OLLAMA_HOST otherwise. The model is the one the Hub handed
# out, else nomic-embed-text; a name outside the index's declared
# generations is refused at boot.
LLM_EMBEDDING_API_BASE: ${LLM_EMBEDDING_API_BASE:-}
OLLAMA_EMBED_HOST: ${OLLAMA_EMBED_HOST:-}
LLM_DEFAULT_EMBEDDING_MODEL: ${LLM_DEFAULT_EMBEDDING_MODEL:-}
# Error reporting is opt-out: on wherever a DSN is set. An unset DSN is not
# a privacy setting, it is no configuration — the API then never contacts
# Sentry. CI_TELEMETRY=off or CI_LOCAL_ONLY=true disable it even when a
# DSN is present. See docs/operations/error-reporting.md.
SENTRY_DSN: ${SENTRY_DSN_API:-}
SENTRY_ENV: ${SENTRY_ENV:-}
CI_TELEMETRY: ${CI_TELEMETRY:-}
CI_LOCAL_ONLY: ${CI_LOCAL_ONLY:-}
CI_SERVER_VERSION: ${CI_SERVER_VERSION:-}
# Where clients should reach the sync engine. /api/powersync/token returns
# this verbatim as `powersync_url`, and refuses to issue a token at all
# while it is empty — so without it sync is a permanent 503 no matter what
# else is running. Only this web role needs it; the worker serves no HTTP.
#
# The gateway proxies the engine at /powersync on the app's own origin
# (infrastructure/gateway/nginx.conf), which is not a convenience: the CSP
# served with the app pins connect-src to 'self', so a client cannot reach
# sync on any other origin.
#
# `:+` and not `:-` — this stays EMPTY unless APP_BASE_URL is set, rather
# than defaulting to a bare "/powersync" that would read as configured and
# hand every client a host-less URL. Sync stays off until the deployment
# says where it is, which is the honest failure. Override POWERSYNC_URL
# directly for a sync service on its own hostname — which then also needs
# that origin added to the CSP.
POWERSYNC_URL: ${POWERSYNC_URL:-${APP_BASE_URL:+${APP_BASE_URL}/powersync}}
# POWERSYNC_JWT_SECRET is deliberately absent here: it is a generated
# secret, so it arrives from /run/secrets/.env.secrets the way
# DATABASE_PASSWORD does. generate-secrets mints it and the powersync
# container derives its base64url JWKS key from that same value.
networks:
- ci
extra_hosts:
- host.docker.internal:host-gateway
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3030/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
depends_on:
generate-secrets:
condition: service_completed_successfully
migrate-database:
condition: service_completed_successfully
database:
condition: service_healthy
restart: unless-stopped
# api, worker and gateway have no definition in docker-compose.yml, so they
# cannot inherit its bounded-logging block — see the note on `database`
# there for why an appliance must not run the json-file driver uncapped.
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
profiles: [ "production" ]
# The headless half of the api: same image, same code, no HTTP. Consumes the
# queues and runs the crons marked role: worker — import stage jobs, whose
# parsers run long synchronous stretches that must not share an event loop
# with interactive requests. Exactly ONE replica: the import recovery sweep
# assumes a single worker. No healthcheck on purpose (nothing depends on it,
# and it serves no port); restart covers crashes, and the staleness sweep
# resumes whatever a crash interrupted.
worker:
image: ci/api
volumes:
- secrets-api:/run/secrets:ro
# The same working-area mount as the api: the worker MOVES staged
# sources into run scratch, so both halves must see one filesystem.
- ${CI_IMPORT_WORK_DIR:-./import-work}:/var/lib/ci-import
environment:
CI_ROLE: worker
CI_IMPORT_SCRATCH_DIR: ${CI_IMPORT_SCRATCH_DIR:-/var/lib/ci-import/scratch}
# The same inference env as the api above: the worker runs the embed
# and utility-model jobs, so it must see the same backend, embedder and
# window, or it embeds on a host the api was steered away from.
LLM_API_BASE: ${LLM_API_BASE:-}
LLM_API_KEY: ${LLM_API_KEY:-}
LLM_DEFAULT_CHAT_MODEL: ${LLM_DEFAULT_CHAT_MODEL:-}
LLM_UTILITY_MODEL: ${LLM_UTILITY_MODEL:-}
CI_LLM_NUM_CTX: ${CI_LLM_NUM_CTX:-}
LLM_EMBEDDING_API_BASE: ${LLM_EMBEDDING_API_BASE:-}
OLLAMA_EMBED_HOST: ${OLLAMA_EMBED_HOST:-}
LLM_DEFAULT_EMBEDDING_MODEL: ${LLM_DEFAULT_EMBEDDING_MODEL:-}
SENTRY_DSN: ${SENTRY_DSN_API:-}
SENTRY_ENV: ${SENTRY_ENV:-}
CI_TELEMETRY: ${CI_TELEMETRY:-}
CI_LOCAL_ONLY: ${CI_LOCAL_ONLY:-}
CI_SERVER_VERSION: ${CI_SERVER_VERSION:-}
networks:
- ci
depends_on:
generate-secrets:
condition: service_completed_successfully
migrate-database:
condition: service_completed_successfully
database:
condition: service_healthy
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
profiles: [ "production" ]
database:
volumes:
- secrets-database:/run/secrets:ro
depends_on:
generate-secrets:
condition: service_completed_successfully
gateway:
image: ci/gateway
ports:
- 8642:8642
environment:
# Reverse proxies allowed to vouch for the client address (see
# infrastructure/gateway/entrypoint.sh). Leave empty on an install whose
# clients reach this port directly.
TRUSTED_PROXY_CIDRS: ${TRUSTED_PROXY_CIDRS:-}
networks:
- ci
depends_on:
api:
condition: service_healthy
object-store:
condition: service_healthy
restart: unless-stopped
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
profiles: [ "production" ]
hbpe-service:
environment:
API_URL: http://api:3030
volumes:
- secrets-hbpe:/run/secrets:ro
depends_on:
generate-secrets:
condition: service_completed_successfully
# NO `devices:`/`group_add:` here on purpose. This file used to pass
# /dev/kfd + /dev/dri and group_add: [video] through unconditionally, which
# meant `make production` could only ever start on an AMD ROCm host: a
# `devices:` entry whose node does not exist is a hard start failure, not a
# graceful degradation, so every Intel/NVIDIA/macOS host was refused before
# the container ran. GPU passthrough lives in docker-compose.rocm.yml, the
# opt-in overlay built for exactly this (it carries the same two device
# nodes plus the numeric AMD_RENDER_GID/AMD_VIDEO_GID groups and
# seccomp:unconfined, which literal `video` never did):
#
# docker compose -f docker-compose.yml -f docker-compose.production.yml \
# -f docker-compose.rocm.yml --profile production up -d
#
# Without the overlay hbpe-service still starts and still answers — it just
# finds no HSA agent and runs on the CPU. See the README's "GPU
# acceleration (HBPE)" section.
object-store:
volumes:
- secrets-object-store:/run/secrets:ro
depends_on:
generate-secrets:
condition: service_completed_successfully
summary-service:
volumes:
- secrets-summary:/run/secrets:ro
environment:
# The same inference env as the api: the sidecar chats and embeds on its
# own, and resolves the embedder's host the same way (config.py
# `resolve_embedding_target`). Unset, it assumes Ollama on the host.
LLM_API_BASE: ${LLM_API_BASE:-}
LLM_API_KEY: ${LLM_API_KEY:-}
LLM_DEFAULT_CHAT_MODEL: ${LLM_DEFAULT_CHAT_MODEL:-}
LLM_EMBEDDING_API_BASE: ${LLM_EMBEDDING_API_BASE:-}
OLLAMA_EMBED_HOST: ${OLLAMA_EMBED_HOST:-}
LLM_DEFAULT_EMBEDDING_MODEL: ${LLM_DEFAULT_EMBEDDING_MODEL:-}
depends_on:
generate-secrets:
condition: service_completed_successfully
#############
# EPHEMERAL #
#############
generate-secrets:
image: ci/setup-secrets
restart: "no"
volumes:
# The master, read-write, and the only place it is mounted.
- secrets:/run/secrets
# One projection volume per service, at the path the entrypoint
# projects into (SECRETS_PROJECTION_ROOT/<service>). Each service
# above mounts its own at /run/secrets.
- secrets-api:/run/secrets/services/api
- secrets-database:/run/secrets/services/database
- secrets-setup-database:/run/secrets/services/setup-database
- secrets-object-store:/run/secrets/services/object-store
- secrets-hbpe:/run/secrets/services/hbpe
- secrets-summary:/run/secrets/services/summary
- secrets-powersync:/run/secrets/services/powersync
- secrets-tooling:/run/secrets/services/tooling
profiles: [ "production" ]
migrate-database:
image: ci/api
volumes:
- secrets-api:/run/secrets:ro
networks:
- ci
command: ["yarn", "migrate"]
restart: "no"
depends_on:
generate-secrets:
condition: service_completed_successfully
database:
condition: service_healthy
profiles: [ "production" ]
setup-database:
volumes:
- secrets-setup-database:/run/secrets:ro
volumes:
db-data:
name: ${COMPOSE_PROJECT_NAME}-production-db-data
object-data:
name: ${COMPOSE_PROJECT_NAME}-production-object-data