SETUP · 03
Run the commands yourself.
The no-CLI alternative — same outcome, no `subwave` binary on your host. Useful if you'd rather not run an installer, are scripting the deploy, want a non-standard layout, or just prefer running each command by hand. These four steps land at a public-facing single-host deploy — Caddy on the edge, Cloudflare in front, internal-only Icecast, Controller, and Web.
If you don't mind a single binary on your host, the standalone CLI collapses these four steps into curl … | sh (which chains init and start behind two Enter prompts) followed by subwave setup — see Quick Start. It uses the same compose images and writes to the same state/ layout; nothing is locked in.
Grab the two files
No clone needed. SUB/WAVE installs from a single docker-compose.yml + a 3-var .env:
mkdir subwave && cd subwave
curl -O https://raw.githubusercontent.com/perminder-klair/subwave/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/perminder-klair/subwave/main/.env.example
mv .env.example .env
$EDITOR .envOnly three keys are required to boot the stack. The rest are collected by the first-run wizard at /onboarding after the containers come up.
# .env (repo root) — three keys, that's the whole boot config.
# Admin gate for /admin + the first-run wizard. REQUIRED in prod.
ADMIN_USER=admin
ADMIN_PASS=replace-me-with-a-strong-string # openssl rand -hex 16
# Public origin — used for OG tags, sitemap, share cards.
SITE_URL=https://radio.example.comClone the repo and run ./scripts/setup.sh — it scaffolds the same .env + sets state-dir perms. Or run npm run setup for an interactive terminal wizard that does the equivalent of the browser flow without ever opening a browser.
Boot the stack
docker compose up -dWhat just started:
- broadcast — icecast2 and liquidsoap together in one container. Generates three random Icecast passwords on first boot, persisted to
state/icecast-secrets.env(noscripts/setup.shstep needed for this); the entrypoint sources them before exec-ing liquidsoap. Internal-only. - controller — the DJ brain; the one talking to Navidrome and your LLM.
- web — Next.js UI, internal-only
- caddy — the only thing bound to a host port (
:7700)
docker-compose.yml pulls ghcr.io/perminder-klair/subwave-*:latest by default. Pin a specific release with SUBWAVE_VERSION=v1.2.3 in your root .env. Add --build to the up command to build from a local clone instead.
Swap the compose file for docker-compose.byo.yml — same stack minus the bundled Caddy, with web / controller / broadcast bound to :7700 / :7701 / :7702.
You must front this with a reverse proxy. The web UI calls /api/* and /stream.mp3 same-origin (those paths are baked into the image at build time). Without a proxy routing them to the controller and Icecast, the page loads but the player is dead — no metadata, no audio. Route table to replicate (mirrors docker/Caddyfile):
/stream.mp3 → host:7702 # disable proxy buffering for live audio
/api/* → host:7701/* # strip the /api prefix
/* → host:7700 # everything else → webIf you need separate hostnames per surface, rebuild the web image with NEXT_PUBLIC_API_URL and NEXT_PUBLIC_STREAM_URL set — those are baked at build time, not runtime.
Finish setup in the browser
open http://localhost:7700/onboarding # or https://your-host/onboardingSign in with the ADMIN_USER / ADMIN_PASS you set in .env. The wizard collects, probes live, and persists:
- Navidrome — URL + user + pass. Saved to
state/setup-config.json. - LLM provider + model — Ollama (homelab default, no key), Anthropic, OpenAI, Google, DeepSeek, OpenRouter, Vercel AI Gateway, or any self-hosted OpenAI-compatible server. Cloud API keys go to
state/secrets.env(mode 0600, sourced intoprocess.envon boot). - TTS engine — Piper (default), Kokoro, cloud (OpenAI / ElevenLabs), or Chatterbox if you built with
--build-arg WITH_CHATTERBOX=1. - DJ persona — station name, location for weather, optional system-prompt override.
- Jingles — one-click button to render 5 default station idents via your chosen TTS engine.
npm run setup walks the same flow without a browser. Same probes, same persistence, same end state. Need git clone + Node 20+ for that path.
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.
npm start opens the operator console — a menu for stack status, a diagnostic sweep, logs, restart, and the terminal player. The everyday way to run the station once it's installed.
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.