SETUP · 03
Run the commands yourself.
The same outcome as the wizard, just without the wizard wrapping it. Useful if you're scripting the install, want a non-standard layout, or just prefer running each command by hand. These six steps land at a public-facing single-host deploy — Caddy on the edge, Cloudflare in front, internal-only Icecast, Controller, and Web.
Clone the repo
git clone https://github.com/perminder-klair/subwave.git
cd subwaveTell the controller where your Navidrome library lives
Copy the template and fill in your values:
cp controller/.env.example controller/.env
$EDITOR controller/.envThe values that actually matter — the rest of the template has good defaults. The LLM provider and model are chosen later, in the admin Settings UI, not here:
# controller/.env — point SUB/WAVE at your services
# Navidrome (or any Subsonic-API server)
NAVIDROME_URL=http://navidrome.local:4533
NAVIDROME_USER=your-username
NAVIDROME_PASS=your-password
# (Optional) If the controller can read your music files directly from
# disk, set this to the mount path — skips streaming over HTTP.
# MUSIC_LIBRARY_PATH=/music
# LLM — the active provider + model are chosen in the admin Settings UI,
# not here. Ollama (the homelab default) needs no key. Only set a cloud
# key below if you switch to a hosted provider in Settings.
# ANTHROPIC_API_KEY=
# OPENAI_API_KEY=
# OPENROUTER_API_KEY=
# DEEPSEEK_API_KEY=
# ELEVENLABS_API_KEY= # only for the 'cloud' TTS voice
# Icecast source password (any string; just match the docker-compose env)
ICECAST_SOURCE_PASSWORD=replace-me-with-a-strong-string
# Admin auth — gates the /admin console. REQUIRED in production (the
# controller refuses to boot without it); optional for local dev.
ADMIN_USER=admin
ADMIN_PASS=replace-meBefore booting the stack, sanity-check Navidrome from your terminal:
curl "$NAVIDROME_URL/rest/ping.view?u=$NAVIDROME_USER&p=$NAVIDROME_PASS&v=1.16.1&c=sub-wave&f=json"You should get back { "subsonic-response": { "status": "ok" ... } }.
Configure the broadcast layer
Icecast needs three passwords and a state directory the containers can share. scripts/setup.sh renders the Icecast config from a template; running it once is enough.
# docker/.env
ICECAST_SOURCE_PASSWORD=replace-me-with-a-strong-string
ICECAST_ADMIN_PASSWORD=another-strong-string
ICECAST_RELAY_PASSWORD=another-strong-string
SUBWAVE_HOMEPAGE=landing
# STATE_DIR=/srv/subwave # optional — defaults to <repo>/statesudo ./scripts/setup.sh # state defaults to <repo>/stateSTATE_DIR is where Liquidsoap, the controller, and the web container exchange files — next track, voice WAVs, now-playing. Anything that survives docker compose down lives there.
Boot the stack
docker compose -f docker/docker-compose.prod.yml up -d --buildWhat just started:
- icecast — broadcast endpoint, internal-only
- liquidsoap — mixer feeding Icecast
- controller — the DJ brain; the one talking to Navidrome and Ollama
- web — Next.js UI, internal-only
- caddy — the only thing bound to a host port (
:4800)
Generate the Piper station idents the first time:
./scripts/generate-jingles.shTune in
open http://localhost:4800Behind a domain? Put it behind Cloudflare or Tailscale; Caddy has auto_https off, so terminate TLS upstream.
Sign in to the admin console at /admin with the ADMIN_USER / ADMIN_PASS you set earlier. Build a roster of DJ personas — each with its own name, soul, voice, and skills — pick the LLM provider, and paint the weekly Shows schedule. Persona changes apply on the next intro, no restart needed.
Verify the broadcast
The repo ships a health probe that checks the containers, hits /api/health and /api/now-playing, and scans recent logs for errors. Run it after any deploy:
./scripts/health-check.shAuto-detects which compose file is live and which host port Caddy is mapped to. Exits 0 if healthy. Safe to wire into cron or a status page.
WHAT'S NEXT
Keep it running.
The stack is on the air. When a new version lands, head to Updates & Help for the rebuild-only-what-changed workflow and the troubleshooting checklist.