forked from perminder-klair/subwave
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.byo.yml
More file actions
271 lines (262 loc) · 11.9 KB
/
Copy pathdocker-compose.byo.yml
File metadata and controls
271 lines (262 loc) · 11.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
# SUB/WAVE — production without the bundled reverse proxy. Use this when you
# already run Traefik/nginx/Caddy: web, controller, and broadcast bind host
# ports (${WEB_PORT:-7700} / ${CONTROLLER_PORT:-7701} / ${ICECAST_PORT:-7702})
# and your proxy fronts them. Liquidsoap stays internal to the broadcast
# container.
#
# Ports bind 0.0.0.0 by default; if your proxy runs on THIS host, set
# BIND_ADDRESS=127.0.0.1 in .env so the services are only reachable on loopback.
#
# The web image is baked for same-origin /api + /stream.mp3, so point your proxy
# at ONE hostname and replicate docker/Caddyfile's route table:
# /api/listener-auth → 404 / deny (do this one FIRST — see below)
# /stream* → host:${ICECAST_PORT} (keep path; disable buffering)
# /listen.pls /listen.m3u → host:${CONTROLLER_PORT} (keep path)
# /api/* → host:${CONTROLLER_PORT} (strip the /api prefix)
# everything → host:${WEB_PORT}
# Split hostnames need a web rebuild with NEXT_PUBLIC_API_URL /
# NEXT_PUBLIC_STREAM_URL (baked at build time).
#
# Block /api/listener-auth at your proxy: it is Icecast's URL-auth callback and
# answers 200/401 on the shared privacy.password, so routing it hands the
# internet a password oracle (#478). Icecast reaches the controller directly
# over the compose network and never needs it through the proxy.
#
# State persists in <repo>/state (override with STATE_DIR); bind mount, so
# `docker compose down -v` won't touch it.
x-state: &state-mount ${STATE_DIR:-./state}:/var/sub-wave
# Cap container log growth (10m × 3 ≈ 30MB/service).
x-logging: &default-logging
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
# -------------------------------------------------------------------------
# BROADCAST — icecast2 + liquidsoap in one container
# -------------------------------------------------------------------------
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:
- 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:-}
# No bundled edge here, so nothing resolves by default and listener rows
# show your proxy's address. Set this to that proxy's IP (as icecast sees
# it) for real listener IPs in admin → Listeners. See docs/deployment.md.
- ICECAST_TRUSTED_PROXY_IPS=${ICECAST_TRUSTED_PROXY_IPS:-}
- ICECAST_TRUSTED_PROXY_HOSTS=${ICECAST_TRUSTED_PROXY_HOSTS:-}
- TZ=${TZ:-Europe/London}
ports:
- "${BIND_ADDRESS:-0.0.0.0}:${ICECAST_PORT:-7702}:7702"
extra_hosts:
- "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, bound to host for your reverse proxy
# -------------------------------------------------------------------------
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:
- NODE_ENV=production
- TZ=${TZ:-Europe/London}
- STATE_DIR=/var/sub-wave
- SOUNDS_DIR=/sounds
# Optional Chatterbox/PocketTTS sidecar (--profile tts-heavy);
# unreachable URL → fall back to Piper.
- TTS_HEAVY_URL=${TTS_HEAVY_URL:-http://tts-heavy:8080}
# Acoustic-analysis sidecar (default-on below). Probed, then local venv.
- ANALYZE_URL=${ANALYZE_URL:-http://analyzer:8080}
# 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:
- ./.env
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
# BIND_ADDRESS=127.0.0.1 keeps the admin API off the network when the
# proxy is same-host (see header).
- "${BIND_ADDRESS:-0.0.0.0}:${CONTROLLER_PORT:-7701}:7701"
volumes:
- *state-mount
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
# -------------------------------------------------------------------------
# Read-only, GET-only, CONTAINERS-section-only slice of the Docker API 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, bound to host for your reverse proxy
# -------------------------------------------------------------------------
web:
image: ghcr.io/perminder-klair/subwave-web:${SUBWAVE_VERSION:-latest}
build:
context: .
dockerfile: web/Dockerfile
args:
- SITE_URL=${SITE_URL:-}
- 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}
- 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.
- GA_ID=${NEXT_PUBLIC_GA_ID:-}
# Server-side base URL for generateMetadata; internal compose name.
- CONTROLLER_INTERNAL_URL=http://controller:7701
ports:
- "${BIND_ADDRESS:-0.0.0.0}:${WEB_PORT:-7700}:7700"
# -------------------------------------------------------------------------
# TTS-HEAVY (optional) — sidecar for Chatterbox + PocketTTS
# -------------------------------------------------------------------------
# NOT started by default (#103):
# docker compose -f docker-compose.byo.yml --profile tts-heavy up -d
# With the profile off, the controller 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) — 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.
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, not via the host
# OOM-killer taking broadcast/controller with it.
mem_limit: ${TTS_HEAVY_MEM_LIMIT:-10g}
profiles: ["tts-heavy"]
environment:
- TTS_HEAVY_DEVICE=${TTS_HEAVY_DEVICE:-cpu}
- POCKET_TTS_VOICE=${POCKET_TTS_VOICE:-alba}
# Which engines to load (comma-separated); each costs RAM + weights.
- TTS_HEAVY_ENGINES=${TTS_HEAVY_ENGINES:-chatterbox,pocket-tts}
# Optional — PocketTTS voice cloning (#238): accept the terms at
# huggingface.co/kyutai/pocket-tts and set HF_TOKEN.
- HF_TOKEN=${HF_TOKEN:-}
volumes:
- *state-mount
# Persist HF caches across recreates, or the multi-GB weight fetch
# repeats every time.
- 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`.
analyzer:
# Default: LEAN multi-arch. ANALYZER_HEAVY=1 in .env → CLAP + Demucs heavy
# image (amd64-only; on arm64 also set DOCKER_DEFAULT_PLATFORM=linux/amd64).
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, not via the host
# OOM-killer taking broadcast/controller with it.
mem_limit: ${ANALYZER_MEM_LIMIT:-6g}
environment:
# Force CLAP / Demucs on for the whole pass; usually 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.
- ANALYZE_DEVICE=${ANALYZE_DEVICE:-}
# Idle seconds before the worker drops CLAP/Demucs models (0 = never;
# default 300 cuda / 1800 cpu — see #1204).
- ANALYZE_IDLE_UNLOAD_S=${ANALYZE_IDLE_UNLOAD_S:-}
# Idle seconds before the sidecar recycles the whole worker process
# (default 3600; 0 = never).
- 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:-}
# Anonymous HF downloads are rate-limited; same var as tts-heavy.
- HF_TOKEN=${HF_TOKEN:-}
volumes:
- *state-mount
- analyzer-cache:/opt/analyzer/hf-cache
volumes:
tts-heavy-chatterbox-cache:
tts-heavy-pocket-cache:
analyzer-cache: