-
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathdocker-compose.example.yml
More file actions
312 lines (296 loc) · 10.6 KB
/
Copy pathdocker-compose.example.yml
File metadata and controls
312 lines (296 loc) · 10.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
name: nojoin
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
x-shared-app-environment: &shared-app-environment
DATABASE_URL: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-nojoin}
REDIS_URL: redis://:${REDIS_PASSWORD:-change_to_secure_string}@redis:6379/0
CELERY_BROKER_URL: redis://:${REDIS_PASSWORD:-change_to_secure_string}@redis:6379/0
CELERY_RESULT_BACKEND: redis://:${REDIS_PASSWORD:-change_to_secure_string}@redis:6379/0
HF_TOKEN: ${HF_TOKEN:-}
DEFAULT_TIMEZONE: ${DEFAULT_TIMEZONE:-UTC}
LLM_PROVIDER: ${LLM_PROVIDER:-gemini}
GEMINI_API_KEY: ${GEMINI_API_KEY:-}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
OLLAMA_API_URL: ${OLLAMA_API_URL:-http://host.docker.internal:11434}
SECONDARY_LLM_PROVIDER: ${SECONDARY_LLM_PROVIDER:-}
SECONDARY_GEMINI_API_KEY: ${SECONDARY_GEMINI_API_KEY:-}
SECONDARY_OPENAI_API_KEY: ${SECONDARY_OPENAI_API_KEY:-}
SECONDARY_ANTHROPIC_API_KEY: ${SECONDARY_ANTHROPIC_API_KEY:-}
SECONDARY_OLLAMA_API_URL: ${SECONDARY_OLLAMA_API_URL:-http://host.docker.internal:11434}
DATA_ENCRYPTION_KEY: ${DATA_ENCRYPTION_KEY:-}
GOOGLE_OAUTH_CLIENT_ID: ${GOOGLE_OAUTH_CLIENT_ID:-}
GOOGLE_OAUTH_CLIENT_SECRET: ${GOOGLE_OAUTH_CLIENT_SECRET:-}
MICROSOFT_OAUTH_CLIENT_ID: ${MICROSOFT_OAUTH_CLIENT_ID:-}
MICROSOFT_OAUTH_CLIENT_SECRET: ${MICROSOFT_OAUTH_CLIENT_SECRET:-}
MICROSOFT_OAUTH_TENANT_ID: ${MICROSOFT_OAUTH_TENANT_ID:-common}
# Shared config for the three worker lanes (worker-gpu / worker-cpu / worker-io).
# They run the same image and differ only in Celery queue, pool, and GPU access.
x-worker-base: &worker-base
image: ghcr.io/valtora/nojoin-worker:latest
volumes:
- ./data:/app/data
- model_cache:/home/appuser/.cache
- /sys/class/drm:/sys/class/drm:ro
- backup_temp:/tmp
depends_on:
init-perms:
condition: service_completed_successfully
db:
condition: service_healthy
redis:
condition: service_healthy
extra_hosts:
- host.docker.internal:host-gateway
restart: unless-stopped
networks:
- nojoin_net
logging: *default-logging
x-worker-environment: &worker-environment
<<: *shared-app-environment
XDG_CACHE_HOME: /home/appuser/.cache
HF_HOME: /home/appuser/.cache/huggingface
OMP_NUM_THREADS: 2
MKL_NUM_THREADS: 2
OPENBLAS_NUM_THREADS: 2
VECLIB_MAXIMUM_THREADS: 2
NUMEXPR_NUM_THREADS: 2
services:
db:
container_name: nojoin-db
image: pgvector/pgvector:pg18-trixie
volumes:
- postgres_data:/var/lib/postgresql
environment:
POSTGRES_USER: ${POSTGRES_USER:-postgres}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
POSTGRES_DB: ${POSTGRES_DB:-nojoin}
healthcheck:
test:
[
"CMD-SHELL",
"pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-nojoin}",
]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- nojoin_net
logging: *default-logging
redis:
container_name: nojoin-redis
image: redis:alpine
command: /bin/sh -ec 'printf "requirepass %s\n" "$$REDIS_PASSWORD" > /tmp/redis.conf && exec redis-server /tmp/redis.conf'
environment:
REDIS_PASSWORD: ${REDIS_PASSWORD:-change_to_secure_string}
REDISCLI_AUTH: ${REDIS_PASSWORD:-change_to_secure_string}
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- nojoin_net
logging: *default-logging
socket-proxy:
container_name: nojoin-socket-proxy
image: tecnativa/docker-socket-proxy
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
CONTAINERS: "1"
POST: "0"
restart: unless-stopped
networks:
- nojoin_net
logging: *default-logging
init-perms:
container_name: nojoin-init-perms
image: busybox:stable
# One-shot, root-only: the api and worker run as non-root (uid 1000), but
# Docker creates a missing bind-mount source (./data) as root on first start,
# which would stop those containers from creating their data subdirectories.
# Ensure the data and recordings directories exist and are owned by uid 1000
# before the app services boot. This runs to completion and exits.
user: "0:0"
command: ["sh", "-c", "mkdir -p /app/data/recordings && chown -R 1000:1000 /app/data"]
volumes:
- ./data:/app/data
restart: "no"
networks:
- nojoin_net
logging: *default-logging
api:
container_name: nojoin-api
image: ghcr.io/valtora/nojoin-api:latest
volumes:
- ./data:/app/data
- ./data/recordings:/app/recordings
- model_cache:/shared_model_cache:ro
- backup_temp:/tmp
environment:
<<: *shared-app-environment
DOCKER_HOST: tcp://socket-proxy:2375
WEB_APP_URL: ${WEB_APP_URL:-https://localhost:14443}
MCP_ENABLED: ${MCP_ENABLED:-true}
FIRST_RUN_PASSWORD: ${FIRST_RUN_PASSWORD:?Set FIRST_RUN_PASSWORD in .env}
XDG_CACHE_HOME: /shared_model_cache
HF_HOME: /shared_model_cache/huggingface
NOJOIN_TRUSTED_PROXIES: ${NOJOIN_TRUSTED_PROXIES:-127.0.0.1,::1,nginx}
depends_on:
init-perms:
condition: service_completed_successfully
db:
condition: service_healthy
redis:
condition: service_healthy
socket-proxy:
condition: service_started
healthcheck:
test:
[
"CMD-SHELL",
"python -c \"import json, sys, urllib.request; req = urllib.request.Request('http://127.0.0.1:8000/api/health', headers={'X-Forwarded-Proto': 'https'}); data = json.load(urllib.request.urlopen(req, timeout=3)); sys.exit(0 if data.get('status') == 'ok' else 1)\"",
]
interval: 10s
timeout: 5s
retries: 12
start_period: 30s
extra_hosts:
- host.docker.internal:host-gateway
restart: unless-stopped
networks:
- nojoin_net
logging: *default-logging
# Worker lanes: work is split by resource profile so a long GPU job never
# blocks lightweight CPU or network tasks. Each service drains one Celery
# queue (see backend/celery_app.py TASK_ROUTES). All three run the same image;
# only the queue, pool, and GPU access differ.
#
# GPU lane: finalize, live ASR, embeddings. Single-slot (--pool=solo
# --concurrency=1) so the one 8 GB card only ever runs one model pipeline at a
# time. This is the ONLY worker with GPU access.
worker-gpu:
<<: *worker-base
container_name: nojoin-worker-gpu
environment:
<<: *worker-environment
NVIDIA_VISIBLE_DEVICES: ${NVIDIA_VISIBLE_DEVICES:-all}
NVIDIA_DRIVER_CAPABILITIES: ${NVIDIA_DRIVER_CAPABILITIES:-compute,utility}
WHISPER_ENABLE_WORD_TIMESTAMPS: ${WHISPER_ENABLE_WORD_TIMESTAMPS:-true}
# Remove this deploy block for CPU-only deployments.
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
command:
[
"celery", "-A", "backend.celery_app.celery_app", "worker",
"-Q", "gpu", "--pool=solo", "--concurrency=1", "--loglevel=info",
]
# CPU lane: ffmpeg segment transcode, proxy generation, backups. No GPU.
worker-cpu:
<<: *worker-base
container_name: nojoin-worker-cpu
environment: *worker-environment
command:
[
"celery", "-A", "backend.celery_app.celery_app", "worker",
"-Q", "cpu", "--pool=prefork", "--concurrency=3", "--loglevel=info",
]
# IO/LLM lane: Meeting Edge, notes, chat embeddings, calendar sync, cleanup.
# Network-bound, so it runs a wider prefork pool. Owns Celery Beat (-B) so the
# periodic jobs fire exactly once across the stack (no double-scheduling).
worker-io:
<<: *worker-base
container_name: nojoin-worker-io
# CLI OAuth AI mode routes inference through a user's Claude subscription via
# the Claude Code CLI + claude-agent-sdk, which need Node.js. Those live only
# in this io-specific image (docker/Dockerfile.worker-io) layered on the
# shared worker image, keeping worker-gpu / worker-cpu lean. The tag is
# published by the release pipeline (M6); until then `docker compose build
# worker-io` builds it locally from the base image via the build stanza.
image: ghcr.io/valtora/nojoin-worker-io:latest
build:
context: .
dockerfile: docker/Dockerfile.worker-io
# worker-io is FROM the shared worker image. Pin that base as a named build
# context so a local `docker compose build worker-io` can never layer it on
# a stale or half-built base. Operators who pull the published images ignore
# this. To build the WHOLE stack from source, give the worker services a
# build: stanza and change this to "service:worker-gpu" -- Compose then
# builds the base image first, then worker-io on top of it.
additional_contexts:
worker_base: "docker-image://ghcr.io/valtora/nojoin-worker:latest"
args:
WORKER_BASE_IMAGE: worker_base
environment: *worker-environment
command:
[
"celery", "-A", "backend.celery_app.celery_app", "worker",
"-Q", "io", "-B", "-s", "/app/data/celerybeat-schedule",
"--pool=prefork", "--concurrency=4", "--loglevel=info",
]
frontend:
container_name: nojoin-frontend
image: ghcr.io/valtora/nojoin-frontend:latest
depends_on:
api:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:14141/"]
interval: 10s
timeout: 5s
retries: 12
start_period: 15s
restart: unless-stopped
networks:
- nojoin_net
logging: *default-logging
nginx:
container_name: nojoin-nginx
image: nginx:alpine
ports:
- "${NOJOIN_BIND_ADDRESS:-127.0.0.1}:14141:80"
- "${NOJOIN_BIND_ADDRESS:-127.0.0.1}:14443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx:/etc/nginx/certs
- ./docker/init-ssl.sh:/docker-entrypoint.d/99-init-ssl.sh
depends_on:
frontend:
condition: service_healthy
api:
condition: service_healthy
healthcheck:
test:
[
"CMD-SHELL",
"curl -k -f -s -o /dev/null https://127.0.0.1/api/health && curl -k -f -s -o /dev/null https://127.0.0.1/",
]
interval: 10s
timeout: 5s
retries: 12
start_period: 10s
restart: unless-stopped
networks:
- nojoin_net
logging: *default-logging
volumes:
postgres_data:
model_cache:
redis_data:
backup_temp:
networks:
nojoin_net:
driver: bridge