VOL. I  ·  NO. 
SUB/WAVE
ON AIR

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.

PREFER THE CLI?

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.

01

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 .env

Only 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.com
PREFER A CLONE?

Clone 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.

02

Boot the stack

docker compose up -d

What just started:

  • broadcast — icecast2 and liquidsoap together in one container. Generates three random Icecast passwords on first boot, persisted to state/icecast-secrets.env (no scripts/setup.sh step 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)
PIN A VERSION

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.

ALREADY RUNNING TRAEFIK OR NGINX?

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 → web

If 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.

03

Finish setup in the browser

open http://localhost:7700/onboarding   # or https://your-host/onboarding

Sign 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 into process.env on 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.
PREFER THE TERMINAL?

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.

04

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.sh

Auto-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.

DAY-TO-DAY OPERATOR CONSOLE

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.