forked from perminder-klair/subwave
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
308 lines (298 loc) · 14.3 KB
/
Copy pathdocker-compose.yml
File metadata and controls
308 lines (298 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
# SUB/WAVE — production orchestration. Only Caddy binds a host port; Cloudflare
# terminates TLS in front. Image-first: services pull baked GHCR images (the
# build: blocks let a checkout rebuild locally). State persists in <repo>/state
# (override with STATE_DIR) — a bind mount, so `down -v` won't touch it, but
# keep it clear of `git clean -dffx`.
x-state: &state-mount ${STATE_DIR:-./state}:/var/sub-wave
# Cap container log growth (10m × 3 ≈ 30MB/service) — the default json-file
# driver is unbounded and eventually fills the host disk.
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
# -------------------------------------------------------------------------
# CADDY — public edge, the only service bound to a host port
# -------------------------------------------------------------------------
caddy:
image: ghcr.io/perminder-klair/subwave-caddy:${SUBWAVE_VERSION:-latest}
build:
context: .
dockerfile: docker/Dockerfile.caddy
container_name: sub-wave-caddy
restart: unless-stopped
logging: *default-logging
depends_on:
# web has no healthcheck (static Next.js server) — started is enough.
# Gate the edge on controller + broadcast health to avoid cold-boot 502s.
web:
condition: service_started
controller:
condition: service_healthy
broadcast:
condition: service_healthy
ports:
- "${CADDY_PORT:-7700}:80"
volumes:
- caddy-data:/data
- caddy-config:/config
# -------------------------------------------------------------------------
# BROADCAST — icecast2 + liquidsoap in one container
# -------------------------------------------------------------------------
# The entrypoint resolves ICECAST_* passwords, renders icecast.xml, and
# launches both; if either dies the container exits and restarts the pair.
broadcast:
image: ghcr.io/perminder-klair/subwave-broadcast:${SUBWAVE_VERSION:-latest}
build:
context: .
dockerfile: docker/Dockerfile.broadcast
container_name: sub-wave-broadcast
restart: unless-stopped
logging: *default-logging
environment:
# Optional — blank means random values generated on first boot and
# persisted to state/icecast-secrets.env (delete that file + restart
# broadcast to rotate).
- ICECAST_SOURCE_PASSWORD=${ICECAST_SOURCE_PASSWORD:-}
- ICECAST_ADMIN_PASSWORD=${ICECAST_ADMIN_PASSWORD:-}
- ICECAST_RELAY_PASSWORD=${ICECAST_RELAY_PASSWORD:-}
# Concurrent-listener ceiling (<limits><clients>). Empty → 100.
- ICECAST_MAX_CLIENTS=${ICECAST_MAX_CLIENTS:-}
# Proxies whose X-Forwarded-For icecast should believe (real listener IPs
# in admin → Listeners). Unset resolves the bundled `caddy` by name at
# render time — lands on every restart but misses the very first cold
# boot (caddy waits on this service's healthcheck). Pin an IP here for
# first-boot accuracy.
- ICECAST_TRUSTED_PROXY_IPS=${ICECAST_TRUSTED_PROXY_IPS:-}
- ICECAST_TRUSTED_PROXY_HOSTS=${ICECAST_TRUSTED_PROXY_HOSTS:-}
# Keeps the hourly archive paths (%Y-%m-%d/%H-00.mp3) on local wall time.
- TZ=${TZ:-Europe/London}
extra_hosts:
# Liquidsoap fetching Subsonic URLs that point at host services
# (e.g. NAVIDROME_URL=http://host.docker.internal:4533).
- "host.docker.internal:host-gateway"
volumes:
- *state-mount
- ${STATE_DIR:-./state}/logs:/var/log/liquidsoap
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:7702/status-json.xsl > /dev/null"]
interval: 5s
timeout: 3s
retries: 12
start_period: 15s
# -------------------------------------------------------------------------
# CONTROLLER — AI DJ brain
# -------------------------------------------------------------------------
controller:
image: ghcr.io/perminder-klair/subwave-controller:${SUBWAVE_VERSION:-latest}
build:
context: .
dockerfile: docker/Dockerfile.controller
args:
# Version reported by the controller; unset → controller/package.json.
- SUBWAVE_BUILD_VERSION=${SUBWAVE_BUILD_VERSION:-}
container_name: sub-wave-controller
restart: unless-stopped
logging: *default-logging
depends_on:
broadcast:
condition: service_healthy
# So a selective `up -d controller` also brings the socket-proxy up.
# Remove this entry too if you drop the proxy below.
docker-socket-proxy:
condition: service_started
environment:
# Enables the production-only admin gate (refuses to boot without
# ADMIN_USER + ADMIN_PASS).
- NODE_ENV=production
# Schedule slots are day-of-week + hour via date.getHours(); without TZ
# the container runs UTC and fires shows an hour early during BST.
- TZ=${TZ:-Europe/London}
- STATE_DIR=/var/sub-wave
- SOUNDS_DIR=/sounds
# Optional Chatterbox/PocketTTS sidecar (--profile tts-heavy). When the
# profile is off the URL is unreachable and TTS falls back to Piper.
- TTS_HEAVY_URL=${TTS_HEAVY_URL:-http://tts-heavy:8080}
# Acoustic-analysis sidecar (default-on below). Probed first, then a
# local venv; falls through to NULL analysis. See music/analyzer.ts.
- ANALYZE_URL=${ANALYZE_URL:-http://analyzer:8080}
# Per-container stats for the admin Stats panel, via the socket-proxy —
# the controller never touches the raw Docker socket. Unset to disable.
- DOCKER_HOST=tcp://docker-socket-proxy:2375
env_file:
# All controller config (Navidrome creds, LLM keys, ADMIN_*, …). Unset
# vars are simply absent — settings.json covers what the wizard manages.
- ./.env
extra_hosts:
- "host.docker.internal:host-gateway"
volumes:
- *state-mount
# curl is in the controller image; /health is served at the router root.
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:7701/health > /dev/null"]
interval: 10s
timeout: 5s
retries: 6
start_period: 20s
# -------------------------------------------------------------------------
# DOCKER-SOCKET-PROXY — locked-down Docker API for the Stats system panel
# -------------------------------------------------------------------------
# Owns the real /var/run/docker.sock and exposes a read-only, GET-only,
# CONTAINERS-section-only slice over internal TCP. Optional: remove it (plus
# the controller's DOCKER_HOST + depends_on entry) to drop the Stats panel.
docker-socket-proxy:
image: ghcr.io/tecnativa/docker-socket-proxy:0.3.0
container_name: sub-wave-docker-proxy
restart: unless-stopped
logging: *default-logging
environment:
- CONTAINERS=1
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
# -------------------------------------------------------------------------
# WEB — Next.js listener UI
# -------------------------------------------------------------------------
web:
image: ghcr.io/perminder-klair/subwave-web:${SUBWAVE_VERSION:-latest}
build:
context: .
dockerfile: web/Dockerfile
args:
# Source builds only — absolute URLs render off the RUNTIME env below.
- SITE_URL=${SITE_URL:-}
# NEXT_PUBLIC_* is inlined into the client bundle at BUILD time.
- NEXT_PUBLIC_GA_ID=${NEXT_PUBLIC_GA_ID:-}
# Admin footer version; unset → web/package.json (web/next.config.js).
- SUBWAVE_BUILD_VERSION=${SUBWAVE_BUILD_VERSION:-}
container_name: sub-wave-web
restart: unless-stopped
logging: *default-logging
depends_on:
# The homepage renders per-request against the controller — starting web
# before it is healthy serves broken first pages.
controller:
condition: service_healthy
environment:
- NODE_ENV=production
- SUBWAVE_HOMEPAGE=${SUBWAVE_HOMEPAGE:-player}
# RUNTIME source of truth for absolute URLs (canonicals, og:url,
# robots.txt, sitemap.xml) — the generic image serves any domain.
- SITE_URL=${SITE_URL:-}
# Set to 1 to keep the shared product pages (landing, docs, news,
# catalogs) self-canonical + in this install's sitemap instead of
# crediting getsubwave.com (web/lib/site.ts IS_OFFICIAL_SITE).
- SUBWAVE_INDEX_ALL=${SUBWAVE_INDEX_ALL:-}
# GA Measurement ID at RUNTIME (web/lib/ga.ts) — analytics turn on with a
# `web` recreate, no rebuild. The build-arg above still bakes it.
- GA_ID=${NEXT_PUBLIC_GA_ID:-}
# Server-side base URL for generateMetadata; internal compose name, not
# exposed to the browser (which uses /api via Caddy).
- CONTROLLER_INTERNAL_URL=http://controller:7701
# -------------------------------------------------------------------------
# TTS-HEAVY (optional) — sidecar for Chatterbox + PocketTTS
# -------------------------------------------------------------------------
# Heavy PyTorch engines kept out of the controller image (#103). NOT started
# by default: `docker compose --profile tts-heavy up -d`. With the profile
# off, TTS_HEAVY_URL is unreachable and the dispatcher falls back to Piper.
tts-heavy:
image: ghcr.io/perminder-klair/subwave-tts-heavy:${SUBWAVE_VERSION:-latest}
build:
context: .
dockerfile: docker/Dockerfile.tts-heavy
args:
# GPU opt-in (source build only): point at a CUDA wheel index and layer
# on docker-compose.tts-heavy-gpu.yml. See docs/gpu-tts.md.
CHATTERBOX_TORCH_INDEX_URL: ${CHATTERBOX_TORCH_INDEX_URL:-https://download.pytorch.org/whl/cpu}
# RTX 50-series only: override chatterbox's torch==2.6.0 pin (no sm_120
# kernels), e.g. CHATTERBOX_TORCH_SPEC="torch==2.9.1 torchaudio==2.9.1".
CHATTERBOX_TORCH_SPEC: ${CHATTERBOX_TORCH_SPEC:-}
# amd64-only image; pinned so it runs under emulation on arm64 hosts.
platform: linux/amd64
container_name: sub-wave-tts-heavy
restart: unless-stopped
logging: *default-logging
# OOM containment: a runaway model load dies here instead of triggering the
# host OOM-killer and taking broadcast/controller down. Default is generous.
mem_limit: ${TTS_HEAVY_MEM_LIMIT:-10g}
profiles: ["tts-heavy"]
environment:
# 'cpu' or 'cuda'; the server falls back to cpu when CUDA is absent.
- TTS_HEAVY_DEVICE=${TTS_HEAVY_DEVICE:-cpu}
# Default PocketTTS voice when a persona doesn't override it.
- POCKET_TTS_VOICE=${POCKET_TTS_VOICE:-alba}
# Which engines to load (comma-separated). Each costs RAM + a first-boot
# weight download; set a single engine if you only use one.
- TTS_HEAVY_ENGINES=${TTS_HEAVY_ENGINES:-chatterbox,pocket-tts}
# Optional — PocketTTS voice CLONING (#238): the cloning weights are
# gated on HF; accept the terms at huggingface.co/kyutai/pocket-tts and
# set HF_TOKEN. Without it, cloned .wav voices revert to a built-in.
- HF_TOKEN=${HF_TOKEN:-}
volumes:
# Shared with the controller — WAVs land in /var/sub-wave/voice/*.
- *state-mount
# Persist HF caches across recreates, or the multi-GB boot-time weight
# fetch repeats on every pull/rebuild.
- tts-heavy-chatterbox-cache:/opt/chatterbox/hf-cache
- tts-heavy-pocket-cache:/opt/pocket-tts/hf-cache
# -------------------------------------------------------------------------
# ANALYZER — acoustic-analysis sidecar (bpm/key/intro/loudness; optional
# CLAP "sounds-like" embeddings + Demucs vocal ranges)
# -------------------------------------------------------------------------
# Starts by default (only the tts-heavy voices stay opt-in). To skip it,
# `docker compose stop analyzer` after boot.
analyzer:
# Default: LEAN multi-arch image. ANALYZER_HEAVY=1 in .env switches to the
# CLAP + Demucs `subwave-analyzer-heavy` image (amd64-only; on arm64 also
# set DOCKER_DEFAULT_PLATFORM=linux/amd64 to run emulated).
image: ghcr.io/perminder-klair/subwave-analyzer${ANALYZER_HEAVY:+-heavy}:${SUBWAVE_VERSION:-latest}
build:
context: .
dockerfile: docker/Dockerfile.analyzer
args:
# Local build mirrors the pulled image: lean unless ANALYZER_HEAVY set.
WITH_CLAP: ${ANALYZER_HEAVY:+1}
WITH_DEMUCS: ${ANALYZER_HEAVY:+1}
container_name: sub-wave-analyzer
restart: unless-stopped
logging: *default-logging
# OOM containment: a runaway analysis dies here instead of triggering the
# host OOM-killer and taking broadcast/controller down. Default is generous.
mem_limit: ${ANALYZER_MEM_LIMIT:-6g}
environment:
# Force CLAP / Demucs on for the whole pass. Usually unnecessary — the
# admin toggles drive these per request.
- ANALYZE_AUDIO_EMBEDDING=${ANALYZE_AUDIO_EMBEDDING:-}
- ANALYZE_VOCAL_ACTIVITY=${ANALYZE_VOCAL_ACTIVITY:-}
# Torch device for CLAP/Demucs: auto (default) / cpu / cuda. Only
# meaningful on the cuda flavour; cpu-wheel images resolve to cpu.
- ANALYZE_DEVICE=${ANALYZE_DEVICE:-}
# Idle seconds before the worker drops CLAP/Demucs models (0 = never).
# Default 300 on cuda (frees VRAM), 1800 on cpu — otherwise one backfill
# parks ~GBs in swap on a small host (#1204).
- ANALYZE_IDLE_UNLOAD_S=${ANALYZE_IDLE_UNLOAD_S:-}
# Idle seconds before the sidecar recycles the whole worker process —
# reclaims the ~1GB of scratch a model release can't return (default
# 3600; 0 = never). Requests racing a recycle queue behind the respawn.
- ANALYZE_RECYCLE_IDLE_S=${ANALYZE_RECYCLE_IDLE_S:-}
- CLAP_MODEL=${CLAP_MODEL:-}
- CLAP_MODEL_PATH=${CLAP_MODEL_PATH:-}
# Demucs model + analysis-window overrides (see .env.example).
- DEMUCS_MODEL=${DEMUCS_MODEL:-}
- ANALYZE_SECONDS=${ANALYZE_SECONDS:-}
- ANALYZE_CLAP_WINDOWS=${ANALYZE_CLAP_WINDOWS:-}
- ANALYZE_OUTRO_SECONDS=${ANALYZE_OUTRO_SECONDS:-}
# CLAP isn't gated, but anonymous HF downloads are rate-limited. Same var
# as tts-heavy — set once and both sidecars pick it up.
- HF_TOKEN=${HF_TOKEN:-}
volumes:
# Shared mount — reads tracks pre-fetched into /var/sub-wave/analyze-tmp.
- *state-mount
# Persist the CLAP/Demucs HF cache across recreates.
- analyzer-cache:/opt/analyzer/hf-cache
volumes:
caddy-data:
caddy-config:
tts-heavy-chatterbox-cache:
tts-heavy-pocket-cache:
analyzer-cache: