-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
498 lines (486 loc) · 22.6 KB
/
Copy pathdocker-compose.yml
File metadata and controls
498 lines (486 loc) · 22.6 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
# SPDX-FileCopyrightText: 2026 soundminds.ai
#
# SPDX-License-Identifier: Apache-2.0
# RelyLoop MVP1 stack (infra_foundation Story 4.2 / FR-1).
#
# Services per docs/01_architecture/deployment.md §"MVP1 deployment shape":
# postgres · redis · migrate (init) · api · worker · elasticsearch · opensearch · ui
#
# Conventions:
# - Host port binds use 127.0.0.1: only — never 0.0.0.0:.
# - Secrets are mounted from ./secrets/<name> via Docker secrets, NEVER as
# bare env vars (CLAUDE.md Absolute Rule #2).
# - Healthchecks gate dependents through `depends_on: condition: service_healthy`.
# - The ``migrate`` init container runs alembic + optuna_schema once at
# boot and exits; api + worker depend on it via
# ``service_completed_successfully`` so the worker never tries to
# CREATE TYPE in the (yet-uncreated) optuna schema
# (bug_worker_optuna_init_race).
#
# `make up` (scripts/install.sh, Story 4.4) auto-generates the required +
# placeholder secret files before invoking `docker compose up -d`.
services:
postgres:
# Pulled third-party service images (postgres / redis / elasticsearch /
# opensearch / solr) are prefixed with ${BASE_REGISTRY} so corp networks
# that block direct docker.io pulls can route them through their mirror —
# the same knob that rewrites the Dockerfile FROM lines. Empty default →
# unchanged Docker Hub behavior. All five live on docker.io, so the one
# prefix covers them. (The api/ui/worker/migrate images are built locally,
# not pulled, so they keep their plain `relyloop/...` tag.)
# See docs/03_runbooks/corporate-network-install.md §1.
image: ${BASE_REGISTRY:-}postgres:17
environment:
POSTGRES_USER: relyloop
POSTGRES_DB: relyloop
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
volumes:
- ./data/postgres:/var/lib/postgresql/data
secrets:
- postgres_password
healthcheck:
test: ["CMD-SHELL", "pg_isready -U relyloop -d relyloop"]
interval: 5s
timeout: 5s
retries: 10
redis:
image: ${BASE_REGISTRY:-}redis:8
volumes:
- ./data/redis:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 10
# Init container — bug_worker_optuna_init_race. Runs alembic +
# optuna_schema once at boot, then exits. api + worker block on its
# successful completion via `service_completed_successfully` so the
# worker's on_startup hook never hits `InvalidSchemaName` from a
# CREATE TYPE in the (yet-uncreated) optuna schema. Reuses the api
# image — no separate Dockerfile.
migrate:
image: relyloop/api:${RELYLOOP_GIT_SHA:-dev}
build:
context: .
dockerfile: Dockerfile
args:
RELYLOOP_GIT_SHA: ${RELYLOOP_GIT_SHA:-dev}
# Corporate-registry overrides for Artifactory-style proxies.
# Defaults are empty / `ghcr.io/` → unchanged behavior. Set the
# env vars in `.env` or inline (`BASE_REGISTRY=artifactory.example.com/ make up`)
# to route base-image pulls through a proxy. See top of Dockerfile.
BASE_REGISTRY: ${BASE_REGISTRY:-}
GHCR_REGISTRY: ${GHCR_REGISTRY:-ghcr.io/}
# Corporate PyPI index mirror for `uv sync`. Default = public PyPI →
# unchanged behavior. Set when the corp network forbids pypi.org (403).
UV_DEFAULT_INDEX: ${UV_DEFAULT_INDEX:-https://pypi.org/simple}
# Corporate HTTP proxy — empty defaults preserve current behavior.
# These are BuildKit predefined ARGs (no Dockerfile ARG declaration
# needed) and are forwarded into RUN steps automatically. Runtime
# egress is set separately via each service's `environment:` block
# so the proxy URL never gets baked into the image. `no_proxy` MUST
# include the Compose service names + `host.docker.internal` (see
# .env.example).
http_proxy: ${http_proxy:-}
https_proxy: ${https_proxy:-}
no_proxy: ${no_proxy:-}
# Optional corporate CA cert — installed into the image's system trust
# store at build time. Empty file = no-op. See top-level `secrets:`
# block + the runbook (docs/03_runbooks/corporate-network-install.md).
secrets:
- corp_ca
command:
- sh
- -c
- "alembic upgrade head && python -m backend.app.db.optuna_schema"
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL_FILE: /run/secrets/database_url
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
# Corporate HTTP proxy (runtime egress). Both case variants because
# Linux tooling is split. Empty default → no proxy.
http_proxy: ${http_proxy:-}
https_proxy: ${https_proxy:-}
no_proxy: ${no_proxy:-}
HTTP_PROXY: ${http_proxy:-}
HTTPS_PROXY: ${https_proxy:-}
NO_PROXY: ${no_proxy:-}
secrets:
- postgres_password
- database_url
volumes:
# Same bind-mount overlays as api so dev-mode migration edits take
# effect without rebuild. Production images carry both via Dockerfile.
- ./migrations:/app/migrations
- ./alembic.ini:/app/alembic.ini:ro
restart: "no"
api:
image: relyloop/api:${RELYLOOP_GIT_SHA:-dev}
build:
context: .
dockerfile: Dockerfile
args:
RELYLOOP_GIT_SHA: ${RELYLOOP_GIT_SHA:-dev}
BASE_REGISTRY: ${BASE_REGISTRY:-}
GHCR_REGISTRY: ${GHCR_REGISTRY:-ghcr.io/}
# Corporate PyPI index mirror for `uv sync`. Default = public PyPI →
# unchanged behavior. Set when the corp network forbids pypi.org (403).
UV_DEFAULT_INDEX: ${UV_DEFAULT_INDEX:-https://pypi.org/simple}
# Corporate HTTP proxy — empty defaults preserve current behavior.
# These are BuildKit predefined ARGs (no Dockerfile ARG declaration
# needed) and are forwarded into RUN steps automatically. Runtime
# egress is set separately via each service's `environment:` block
# so the proxy URL never gets baked into the image. `no_proxy` MUST
# include the Compose service names + `host.docker.internal` (see
# .env.example).
http_proxy: ${http_proxy:-}
https_proxy: ${https_proxy:-}
no_proxy: ${no_proxy:-}
# Optional corporate CA cert — see migrate service.
secrets:
- corp_ca
# Reach a host-native Ollama (feat_bundled_llm_native_detection,
# RELYLOOP_LLM=ollama). No-op on Mac/Windows where host.docker.internal
# already resolves; on Linux it maps to the host bridge (host-gateway).
# Requires Docker Engine >= 20.10 / Compose v2.
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
migrate:
condition: service_completed_successfully
environment:
DATABASE_URL_FILE: /run/secrets/database_url
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
REDIS_URL: redis://redis:6379/0
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1}
OPENAI_API_KEY_FILE: /run/secrets/openai_key
OPENAI_MODEL: ${OPENAI_MODEL:-gpt-4o-2024-08-06}
OPENAI_MODEL_CHAT: ${OPENAI_MODEL_CHAT:-gpt-4o-mini-2024-07-18}
CLUSTER_CREDENTIALS_FILE: /run/secrets/cluster_credentials
RELYLOOP_GIT_SHA: ${RELYLOOP_GIT_SHA:-dev}
# bug_healthz_degraded_blocks_ui_engine_subset — pass the operator's
# engine selection (install.sh derives COMPOSE_PROFILES from
# RELYLOOP_ENGINES) into the api so /healthz treats an
# intentionally-excluded engine's unreachability as non-blocking
# ("not_selected") instead of degraded. Default es,os,solr preserves
# the all-engines behavior when COMPOSE_PROFILES is unset.
COMPOSE_PROFILES: ${COMPOSE_PROFILES:-es,os,solr}
# Corporate HTTP proxy (runtime egress — OpenAI, GitHub, registered
# cluster HTTP). Empty default → no proxy. Both case variants.
http_proxy: ${http_proxy:-}
https_proxy: ${https_proxy:-}
no_proxy: ${no_proxy:-}
HTTP_PROXY: ${http_proxy:-}
HTTPS_PROXY: ${https_proxy:-}
NO_PROXY: ${no_proxy:-}
ports:
- "127.0.0.1:8000:8000"
secrets:
- postgres_password
- database_url
- openai_key
- cluster_credentials
volumes:
- ./data/repo-clones:/var/lib/relyloop/repos
# Bind-mount migrations + alembic.ini so `make migrate-create` (which
# runs inside this container — see Makefile) writes generated revision
# files back to the host repo, and so host-side tweaks to alembic.ini
# (e.g. disabling the ruff post-write hook that requires dev deps) take
# effect without a rebuild. The Dockerfile COPYs both at build time for
# production deployments; these overlays only matter in dev.
- ./migrations:/app/migrations
- ./alembic.ini:/app/alembic.ini:ro
# Bind-mount samples/ read-only so `make seed-es` (which exec's
# backend.app.scripts.seed_es inside this container) can read
# samples/products.json from the host repo without baking the operator
# data into the image. chore_tutorial_polish Story 2.1.
- ./samples:/app/samples:ro
# docker/solr/configsets is read by seed_solr_products.py (run inside
# this container via `make seed-solr`) to upload Solr configsets to
# ZooKeeper before creating collections.
- ./docker/solr/configsets:/app/docker/solr/configsets:ro
healthcheck:
# `worker` waits for api healthy before starting (so it doesn't enqueue
# against a not-yet-started API). `curl` is in the runtime image.
test: ["CMD-SHELL", "curl -fs http://localhost:8000/healthz || exit 1"]
interval: 10s
timeout: 5s
retries: 12
start_period: 30s
worker:
image: relyloop/api:${RELYLOOP_GIT_SHA:-dev}
build:
context: .
dockerfile: Dockerfile
args:
RELYLOOP_GIT_SHA: ${RELYLOOP_GIT_SHA:-dev}
BASE_REGISTRY: ${BASE_REGISTRY:-}
GHCR_REGISTRY: ${GHCR_REGISTRY:-ghcr.io/}
# Corporate PyPI index mirror for `uv sync`. Default = public PyPI →
# unchanged behavior. Set when the corp network forbids pypi.org (403).
UV_DEFAULT_INDEX: ${UV_DEFAULT_INDEX:-https://pypi.org/simple}
# Corporate HTTP proxy — empty defaults preserve current behavior.
# These are BuildKit predefined ARGs (no Dockerfile ARG declaration
# needed) and are forwarded into RUN steps automatically. Runtime
# egress is set separately via each service's `environment:` block
# so the proxy URL never gets baked into the image. `no_proxy` MUST
# include the Compose service names + `host.docker.internal` (see
# .env.example).
http_proxy: ${http_proxy:-}
https_proxy: ${https_proxy:-}
no_proxy: ${no_proxy:-}
# Optional corporate CA cert — installed into the image's system trust
# store at build time. Empty file = no-op. See top-level `secrets:`
# block + the runbook (docs/03_runbooks/corporate-network-install.md).
secrets:
- corp_ca
command: ["arq", "backend.workers.all.WorkerSettings"]
# Reach a host-native Ollama (feat_bundled_llm_native_detection). See api.
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
api:
condition: service_healthy
migrate:
condition: service_completed_successfully
environment:
DATABASE_URL_FILE: /run/secrets/database_url
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
REDIS_URL: redis://redis:6379/0
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1}
OPENAI_API_KEY_FILE: /run/secrets/openai_key
OPENAI_MODEL: ${OPENAI_MODEL:-gpt-4o-2024-08-06}
OPENAI_MODEL_CHAT: ${OPENAI_MODEL_CHAT:-gpt-4o-mini-2024-07-18}
CLUSTER_CREDENTIALS_FILE: /run/secrets/cluster_credentials
# Corporate HTTP proxy (runtime egress — same shape as api).
http_proxy: ${http_proxy:-}
https_proxy: ${https_proxy:-}
no_proxy: ${no_proxy:-}
HTTP_PROXY: ${http_proxy:-}
HTTPS_PROXY: ${https_proxy:-}
NO_PROXY: ${no_proxy:-}
secrets:
- postgres_password
- database_url
- openai_key
- cluster_credentials
volumes:
- ./data/repo-clones:/var/lib/relyloop/repos
# bug_demo_reseed_fake_metric_regression — the rich demo scenario
# reads samples/products.json + samples/queries.csv +
# samples/templates/product_search.j2 from inside the worker
# process. Mount the directory read-only (matches the API service's
# mount above).
- ./samples:/app/samples:ro
# docker/solr/configsets is read by seed_solr_products.py (run inside
# this container via `make seed-solr`) to upload Solr configsets to
# ZooKeeper before creating collections.
- ./docker/solr/configsets:/app/docker/solr/configsets:ro
ui:
# NEXT_PUBLIC_API_BASE_URL is build-time (Next.js bakes NEXT_PUBLIC_*
# into the client bundle at `pnpm build`). Compose `environment:` would
# have no effect — see chore_tutorial_polish §3 (decision log 2026-05-12 M3).
image: relyloop/ui:${RELYLOOP_GIT_SHA:-dev}
build:
context: ./ui
args:
NEXT_PUBLIC_API_BASE_URL: "http://localhost:8000"
# Corporate-registry override for Artifactory-style proxies (see top
# of ui/Dockerfile). Default empty → unchanged Docker Hub behavior.
BASE_REGISTRY: ${BASE_REGISTRY:-}
# Corporate npm registry mirror for `npm`/`pnpm install`. Default =
# public npm → unchanged behavior. Set when the corp network forbids
# registry.npmjs.org (the `npm error code E403` failure mode).
NPM_CONFIG_REGISTRY: ${NPM_CONFIG_REGISTRY:-https://registry.npmjs.org/}
# Forwarded for the OCI `org.opencontainers.image.revision` label.
RELYLOOP_GIT_SHA: ${RELYLOOP_GIT_SHA:-dev}
# Corporate HTTP proxy — same shape as the backend services.
# BuildKit predefined ARGs (no Dockerfile ARG/ENV); runtime is set
# via the `environment:` block below.
http_proxy: ${http_proxy:-}
https_proxy: ${https_proxy:-}
no_proxy: ${no_proxy:-}
# Optional corporate CA cert — same as backend services.
secrets:
- corp_ca
container_name: relyloop-ui-1
restart: unless-stopped
depends_on:
api:
condition: service_healthy
environment:
# Corporate HTTP proxy (runtime egress). Empty default → no proxy.
# SSR calls to the API are Compose-internal — covered by `api` in no_proxy.
http_proxy: ${http_proxy:-}
https_proxy: ${https_proxy:-}
no_proxy: ${no_proxy:-}
HTTP_PROXY: ${http_proxy:-}
HTTPS_PROXY: ${https_proxy:-}
NO_PROXY: ${no_proxy:-}
ports:
- "127.0.0.1:3000:3000"
healthcheck:
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:3000/', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))\""]
interval: 10s
timeout: 3s
retries: 3
start_period: 30s
elasticsearch:
# Gated by Compose profile "es" so operators can opt into a subset of
# engines via RELYLOOP_ENGINES (parsed by scripts/install.sh into
# COMPOSE_PROFILES) — feat_selective_engine_startup_and_demo Story 1.1.
# install.sh defaults RELYLOOP_ENGINES=es,os,solr when unset, so a bare
# `make up` preserves today's three-engine startup. Running
# `docker compose up -d` directly (bypassing make up) WITHOUT setting
# COMPOSE_PROFILES will skip all three engines — documented in
# docs/03_runbooks/local-dev.md "Selecting a subset of engines".
profiles: ["es"]
# Image tag interpolation — feat_engine_version_selection FR-1. Default
# tag matches ENGINE_VERSION_MATRIX["elasticsearch"][0] (the latest
# patch of the latest supported major) at
# backend/app/core/engine_versions.py. CI guard
# scripts/ci/verify_engine_version_matrix_parity.sh enforces sync.
image: ${BASE_REGISTRY:-}elasticsearch:${ES_IMAGE_TAG:-9.4.1}
environment:
- discovery.type=single-node
- xpack.security.enabled=false # local dev only — production install activates at GA v1
- ES_JAVA_OPTS=-Xms${ES_HEAP_SIZE:-512m} -Xmx${ES_HEAP_SIZE:-512m}
# Disable disk-watermark allocation gating for the single-node dev/CI
# cluster. GHA runners boot with ~9% free disk (~6.8 GB / 71 GB) which
# trips ES's high watermark (default 90%) and causes ES to refuse to
# allocate the products index's primary shard — status=red,
# active_primary_shards=0, initializing_shards=0. The watermark behavior
# is by-design on multi-node clusters (relocate shards off a full node)
# but harmful on single-node where there's nowhere to relocate. See
# bug_smoke_seed_es_unavailable_shards_race PR #297 for the diagnosis
# (smoke-logs from run 26612512222 showed the disk-watermark WARN
# immediately before the failed allocation).
- cluster.routing.allocation.disk.threshold_enabled=false
ports:
- "127.0.0.1:9200:9200"
healthcheck:
test: ["CMD-SHELL", "curl -fs http://localhost:9200/_cluster/health || exit 1"]
interval: 10s
timeout: 5s
retries: 6
opensearch:
# Gated by Compose profile "os" — see elasticsearch service above for
# the full rationale (feat_selective_engine_startup_and_demo Story 1.1).
profiles: ["os"]
# Image tag interpolation — feat_engine_version_selection FR-1.
# Default tag matches ENGINE_VERSION_MATRIX["opensearch"][0].
image: ${BASE_REGISTRY:-}opensearchproject/opensearch:${OS_IMAGE_TAG:-3.6.0}
environment:
- discovery.type=single-node
- DISABLE_SECURITY_PLUGIN=true # local dev only
- OPENSEARCH_JAVA_OPTS=-Xms${ES_HEAP_SIZE:-512m} -Xmx${ES_HEAP_SIZE:-512m}
ports:
- "127.0.0.1:9201:9200"
healthcheck:
test: ["CMD-SHELL", "curl -fs http://localhost:9200 || exit 1"]
interval: 10s
timeout: 5s
retries: 6
# Apache Solr (MVP2 / infra_adapter_solr Story A10).
#
# Security is OFF for local dev — the same posture as the local
# elasticsearch (xpack.security.enabled=false) and opensearch
# (DISABLE_SECURITY_PLUGIN=true) services above, per CLAUDE.md
# "Common Pitfalls": local Compose runs the engines security-disabled;
# production-mode auth is a separate (MVP3+) concern. Solr's default is
# no `security.json` => no authentication, so we just run the stock
# image. The SolrAdapter's solr_basic/solr_apikey support still works
# against real operator clusters that DO enable auth; a security-disabled
# Solr simply ignores the Authorization header the adapter sends.
#
# seed_solr_products.py (run via `make seed-solr`) uploads the
# relyloop_products / relyloop_ubi configsets to ZooKeeper, then creates
# the collections.
#
# SOLR_MODULES=ltr loads the Learning-to-Rank module so the products
# configset's LTR queryParser resolves (Solr 10 dropped the old `<lib>`
# directive in solrconfig.xml — modules MUST come from this env var). The
# stock image ships no `ubi` module, so UBI is NOT listed here; the demo
# synthesizes UBI events directly into the ubi_queries/ubi_events
# collections rather than relying on the live solr.UBIComponent.
solr:
# Gated by Compose profile "solr" — see elasticsearch service above for
# the full rationale (feat_selective_engine_startup_and_demo Story 1.1).
profiles: ["solr"]
# Image tag interpolation — feat_engine_version_selection FR-1.
# Default tag matches ENGINE_VERSION_MATRIX["solr"][0].
image: ${BASE_REGISTRY:-}solr:${SOLR_IMAGE_TAG:-10.0}
environment:
SOLR_HEAP: ${SOLR_HEAP_SIZE:-512m}
SOLR_MODULES: ltr
volumes:
- ./data/solr:/var/solr
ports:
- "127.0.0.1:8983:8983"
healthcheck:
test: ["CMD-SHELL", "curl -fs http://localhost:8983/solr/admin/info/system || exit 1"]
interval: 10s
timeout: 5s
retries: 6
start_period: 30s
ollama:
# Bundled local LLM — feat_bundled_local_llm. OFF by default; opted in with
# `RELYLOOP_LLM=ollama make up` (install.sh appends the "bundled-llm"
# profile via scripts/lib/relyloop_llm.sh). Serves an OpenAI-compatible
# /v1 endpoint at http://ollama:11434 on the Compose network; install.sh
# points the api/worker OPENAI_BASE_URL at it. No host port is published —
# the app reaches it in-network, and not binding :11434 avoids colliding
# with a host-native Ollama (the Phase-2 detection target).
profiles: ["bundled-llm"]
# Pinned, non-`latest` (mirrors the engines' pinned-tag convention). Bump
# deliberately + re-run the LLM-compatibility release gate when updating.
image: ${BASE_REGISTRY:-}ollama/ollama:${OLLAMA_IMAGE_TAG:-0.30.10}
entrypoint: ["/bin/sh", "/entrypoint.sh"]
environment:
# Container-side model tag. The `${OLLAMA_MODEL:-…}` default is resolved
# by Compose from the host/.env; the entrypoint + healthcheck read the
# in-container OLLAMA_MODEL at runtime (see the `$$` in the healthcheck).
OLLAMA_MODEL: ${OLLAMA_MODEL:-qwen3.5:4b}
volumes:
- ./docker/ollama/entrypoint.sh:/entrypoint.sh:ro
- ./data/ollama:/root/.ollama
healthcheck:
# `$$OLLAMA_MODEL` (double-dollar) so Compose passes a literal
# `$OLLAMA_MODEL` to the container shell, which reads the CONTAINER env —
# NOT host-interpolated at config-render time. `ollama show` only
# succeeds once the model has finished pulling (gates `up --wait`).
test: ["CMD-SHELL", "ollama show \"$${OLLAMA_MODEL}\" >/dev/null 2>&1 || exit 1"]
interval: 15s
timeout: 10s
retries: 10
# Generous start_period: the first run pulls a multi-GB model. Failing
# checks during this window don't count toward `retries`, so a slow-but-
# healthy network doesn't false-fail `docker compose up -d --wait`.
start_period: 600s
secrets:
postgres_password:
file: ./secrets/postgres_password
database_url:
file: ./secrets/database_url
openai_key:
file: ./secrets/openai_key
cluster_credentials:
file: ./secrets/cluster_credentials.yaml
# Optional corporate CA certificate (PEM format). Build-time only — used
# via `build.secrets` on each service; the Dockerfile copies it into the
# system trust store via `update-ca-certificates`. Empty file → no-op.
# scripts/install.sh creates an empty placeholder so Compose's
# `secrets:` validation doesn't fail at startup. See
# docs/03_runbooks/corporate-network-install.md.
corp_ca:
file: ./secrets/corp_ca.crt