-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
212 lines (204 loc) · 9.36 KB
/
Copy pathdocker-compose.yml
File metadata and controls
212 lines (204 loc) · 9.36 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
# The compose project name, pinned. Without it the project is named after the
# DIRECTORY the file sits in, so renaming a checkout silently orphans whatever
# is running: `docker compose down` under the new name does not see the old
# containers, and `up` starts a second stack that then collides on the port.
# Pinned, the stack keeps one identity wherever it is cloned.
name: visor-serve
# Shared by the two server services below, which differ only in whether they
# get the GPU. Two services rather than one, because a compose device
# reservation is ALL-OR-NOTHING: a service asking for an nvidia device cannot
# start on a host that has none, and an override file cannot rescue it either
# (compose merges the `devices` list rather than replacing it, so `devices: []`
# leaves the reservation in place). `inference` stays the default;
# `inference-cpu` is what scripts/server_ctl.py starts on a machine with no card.
x-inference-base: &inference-base
image: ghcr.io/jules-gp/lab-ai:2026.08
shm_size: 8gb
working_dir: /workspace/server
volumes:
- ./server:/workspace/server
- ./DATA:/data:ro
ports:
# Unset BIND_ADDR publishes on every interface, which is what a lab GPU
# server behind a TLS terminator wants. server_ctl.py writes
# BIND_ADDR=127.0.0.1 for a local install instead: that one speaks plain
# HTTP, and plain HTTP carrying medical images must not leave the machine.
#
# The `:+` form keeps "unset" meaning NO host address rather than the
# literal 0.0.0.0: with no address docker publishes on both stacks, while
# an explicit 0.0.0.0 is IPv4 only and would silently drop IPv6 clients.
# The inner `:-` is not redundant -- without it compose warns "The
# BIND_ADDR variable is not set" on every command.
#
# HOST_PORT moves the HOST side only; the container always serves 8000.
- "${BIND_ADDR:+${BIND_ADDR:-}:}${HOST_PORT:-8000}:8000"
# The install must NOT gate uvicorn. `pip install -r requirements.txt` cannot
# succeed offline even when everything is already installed: nnunetv2 ->
# batchgenerators -> unittest2 -> argparse, and pip never treats `argparse` as
# satisfied because the stdlib module shadows the distribution -- so it
# re-downloads that 23 kB wheel on every start. With `&&`, a machine off the
# network had a server that started once and never again.
#
# A genuinely missing dependency still fails loudly: the `import fastapi,
# uvicorn` probe separates "pip could not run but everything is here" (warn,
# start) from "pip could not run AND nothing is installed" (one FATAL line,
# exit 1). Without it the second case was an ImportError traceback restarted
# for ever by `restart: unless-stopped`. server_ctl.py greps for both markers.
#
# LAYOUT: every operator sits at the END of its line. A `>` folded scalar
# keeps the newlines of its continuation lines, so a line STARTING with `||`
# reaches sh as its own command and is a syntax error.
command: >
sh -c "pip install --no-cache-dir --user --retries 1 --timeout 20 -r requirements.txt ||
echo 'DEPENDENCY-INSTALL-SKIPPED: pip failed (offline?); continuing with what is already installed' >&2;
python -c 'import fastapi, uvicorn' ||
{ echo 'DEPENDENCY-INSTALL-FATAL: fastapi is not importable and pip could not install it. This container needs network access once.' >&2; exit 1; };
uvicorn main:app --host 0.0.0.0 --port 8000 --reload"
restart: unless-stopped
# Shared by the two test services, which differ only in whether they get the
# GPU. An anchor rather than a copy, so the image, the mounts and the pytest
# command cannot drift between the suite the hook runs and the one a maintainer
# runs.
x-test-base: &test-base
image: ghcr.io/jules-gp/lab-ai:2026.08
profiles: ["test"]
working_dir: /workspace/server
volumes:
- ./server:/workspace/server
- ./DATA:/data:ro
# Read-only, and only for the tests: a few of them check that the code and
# scripts/data-manifest.yml still agree on where a bundle is downloaded.
# The server itself never reads this file.
- ./scripts:/workspace/scripts:ro
# Likewise: the tests check that the image's build fixtures still describe
# their own source, and that the API requirements stay slim.
- ./docker:/workspace/docker:ro
command: >
sh -c "pip install --no-cache-dir --user -r requirements.txt -r requirements-dev.txt &&
python -m pytest"
services:
inference:
<<: *inference-base
environment:
- API_TOKEN=${API_TOKEN:-dev-token}
- DATA_DIR=/data
- DEVICE=${DEVICE:-cuda}
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
# The same server without the card: every tool falls back to CPU through
# settings.DEVICE, only much slower. The "cpu" profile keeps `docker compose
# up` from starting it alongside `inference` (they both bind port 8000):
#
# docker compose --profile cpu up -d inference-cpu
#
# or let scripts/server_ctl.py pick between the two from what the host has.
inference-cpu:
<<: *inference-base
profiles: ["cpu"]
environment:
- API_TOKEN=${API_TOKEN:-dev-token}
- DATA_DIR=/data
- DEVICE=cpu
# The pytest suite in the same environment as `inference`, without the GPU
# reservation: asking for a GPU here would make the pre-push hook fail on any
# clone without an nvidia card, and the unit tests stub every model anyway.
# The "test" profile keeps it out of `docker compose up`; run it with
# `docker compose run --rm test`.
test:
<<: *test-base
environment:
- API_TOKEN=test-token
- DATA_DIR=/data
- DEVICE=cpu
# RUN_REAL_DATA_TESTS is deliberately NOT set: tests/test_data_integration.py
# runs real inference against whatever is under DATA/, which is hours on a
# CPU once a full ALI bundle is present. This suite stays seconds long.
# The same suite with the card, for the one part that wants it: the real-data
# suite is minutes on a GPU and hours on a CPU.
#
# docker compose run --rm test-gpu
#
# A separate service rather than a flag on `test`, the reservation being
# all-or-nothing -- which is why the hook keeps pointing at `test`.
test-gpu:
<<: *test-base
environment:
- API_TOKEN=test-token
- DATA_DIR=/data
- DEVICE=cuda
# The whole reason this service exists: the real-data suite.
- RUN_REAL_DATA_TESTS=1
shm_size: 8gb
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
# The venvs image (docker/Dockerfile): ONE container, N virtualenvs, and a
# server that imports no tool at all. Under a profile, so nothing about the
# deployment above changes:
#
# docker compose --profile venvs up -d --build inference-venvs
#
# `tools` is where the packaged tool folders come from -- the SADT-VISOR
# repository's output. It defaults to the build fixtures, which need no GPU
# and no model and exist to prove the layout works:
#
# TOOLS_CONTEXT=../SADT-VISOR docker compose --profile venvs build
#
# The ROOT of the checkout, not a dist/ subdirectory: the build copies
# `tools` and `scripts` out of that context, and both sit at the top.
#
# No GPU reservation here yet: the fixtures do not need one, and a
# reservation is all-or-nothing (it would make this service unstartable on
# any machine without a card). A real deployment adds the same `deploy:`
# block as `inference`.
inference-venvs:
profiles: ["venvs"]
build:
context: .
dockerfile: docker/Dockerfile
additional_contexts:
tools: ${TOOLS_CONTEXT:-docker/fixtures}
environment:
- API_TOKEN=${API_TOKEN:-dev-token}
- DEVICE=${DEVICE:-cuda}
volumes:
# Read-only, and outside the image: an image gets copied around, a mount
# does not.
- ./DATA:/DATA:ro
# deployment.toml is per-INSTALLATION config: which tool reads which DATA
# folder, what each may upload, how long each may run. Baked into the
# image, changing one line costs a 22 GB rebuild, and a published image
# could not be configured at all. Mounted over the image's own copy, which
# stays the default for a deployment that mounts nothing here.
- ./server/deployment.toml:/opt/sadt/server/deployment.toml:ro
# Port 8001 by default, so it can run beside the in-process server while
# the tools are moved over one at a time.
ports:
- "${BIND_ADDR:+${BIND_ADDR:-}:}${VENVS_HOST_PORT:-8001}:8000"
# Docker gives a container 64 MB, and nnUNet passes data between its
# export workers through shared memory. Without this AMASSS and
# Batch_Dental_Seg die with "Background workers died ... your RAM was
# full", which names the wrong resource: the host had 100 GB free.
shm_size: 8gb
# The tools this image carries are the real ones, not the build fixtures
# the comment above was written for: AMASSS, ALI and Crown_Seg all
# expect a card. A reservation is all-or-nothing, so this service no
# longer starts without one, which is correct for an image whose
# purpose is running them.
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
restart: unless-stopped