10 KiB
Bordanlage – Boat Onboard System
Multiroom audio + navigation dashboard for boats. Full Docker stack — works on any machine, no hardware required in dev mode.
Stack
| Service | Image / Build | Purpose |
|---|---|---|
| SignalK | signalk/signalk-server |
NMEA 0183/2000 gateway, WebSocket delta stream |
| Snapserver | custom build (v0.35.0) | Multiroom audio server, JSON-RPC on :1705 |
| zone-salon/cockpit/bug/heck | custom build (v0.35.0) | Audio zones (snapclient) |
| Mopidy + Iris | custom Dockerfile | Local music, TuneIn radio, HTTP control on :6680 |
| librespot | custom build (v0.5.0) | Spotify Connect receiver, pipe backend |
| shairport-sync | mikebrady/shairport-sync |
AirPlay 2 receiver, pipe backend |
| Jellyfin | jellyfin/jellyfin |
Media library, :8096 |
| SignalK | signalk/signalk-server |
Navigation data, demo NMEA in dev mode |
| Portainer | portainer/portainer-ce |
Docker management UI, :9000 |
| Dashboard | React 18 + Vite | Touch UI, built with Vite (dev: HMR on :8090) |
Audio flow: librespot / shairport / mopidy → named pipe (PCM) → snapserver → snapclient zones
Quick Start (Dev)
make dev
# or without make:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
Dashboard with hot-reload: http://localhost:8090
Everything runs in the browser - no additional software needed!
On first run, Docker builds the custom images (snapserver, snapclient, mopidy, librespot stub). Subsequent starts are instant.
Browser-Only Testing (Windows/Mac)
All services are accessible via browser:
- Dashboard (Main UI): http://localhost:8090 - Full boat control interface
- Snapcast Web: http://localhost:1780 - Audio zones + browser playback via Web Audio API
- Mopidy/Iris: http://localhost:6680/iris/ - Music player
- SignalK: http://localhost:3000 - Navigation backend (admin/bordanlage)
Audio in browser: Open Snapcast Web UI → Click 🔊 icon → Enable Web Audio playback!
Native audio output
Mac:
make mac-audio # runs snapclient natively via Homebrew → Mac speakers
make spotify # runs librespot natively via Homebrew → Spotify Connect on Mac
Both commands require brew install snapcast / brew install librespot (auto-installed if missing).
Windows:
make windows-audio # runs snapclient natively → Windows speakers (WASAPI)
Requires manual install: Download from https://github.com/badaix/snapcast/releases
See WINDOWS_AUDIO_SETUP.md for detailed Windows setup instructions.
Service URLs
| Service | URL | Notes |
|---|---|---|
| Dashboard (dev) | http://localhost:8090 | Vite HMR, live reload |
| Dashboard (prod) | http://localhost:8080 | nginx-served built bundle |
| SignalK | http://localhost:3000 | Admin: admin / bordanlage |
| Mopidy/Iris | http://localhost:6680/iris/ | Music player UI |
| Snapcast Web | http://localhost:1780 | Zone control (Snapweb) |
| Jellyfin | http://localhost:8096 | Media library |
| Portainer | http://localhost:9000 | Docker management |
Make Targets
make dev # start dev stack (Vite HMR, null audio, SignalK demo data)
make boot # start production stack (full audio pipeline)
make stop # stop dev stack
make stop-boot # stop production stack
make logs # tail logs (dev stack)
make rebuild # force rebuild all images (dev stack)
make status # show container status
make pipes # create audio named pipes in /tmp/audio (for boot mode)
make mac-audio # run snapclient natively on Mac (Homebrew)
make spotify # run librespot natively on Mac (Homebrew, Spotify Connect)
Spotify Connect
Dev mode (Mac): run make spotify — shows as "Bordanlage" in Spotify app.
Boot mode (boat): librespot runs inside Docker, pipe backend, writes PCM to /tmp/audio/spotify.pcm.
On Linux (boat), set
network_mode: hostforlibrespotandshairportindocker-compose.ymlfor reliable mDNS/Bonjour discovery.
AirPlay
The shairport container runs shairport-sync with pipe output.
- On iPhone/Mac: Control Center → AirPlay → select "Bordanlage AirPlay"
- Audio goes through the snapserver multiroom pipeline
Config: config/shairport.conf
Adding Music (Mopidy)
Drop files into ./music/. Trigger a library scan:
curl -s http://localhost:6680/mopidy/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"local.scan"}'
Zones
Four zones are pre-configured:
| Zone | hostID | Production use |
|---|---|---|
| Salon | zone-salon | Below-deck salon speakers |
| Cockpit | zone-cockpit | Cockpit/helm area |
| Bug | zone-bug | Forward cabin (Bug = bow in German) |
| Heck | zone-heck | Stern (Heck = stern in German) |
Each zone runs snapclient connecting to snapserver on the Docker internal network.
In dev mode the zones use --player file:filename=null (audio discarded, no hardware needed).
In production, replace with --player alsa --soundcard hw:N,0 and expose /dev/snd.
Environment Variables
Copy .env.example to .env and adjust:
SPOTIFY_NAME=Bordanlage
SPOTIFY_BITRATE=320
MUSIC_PATH=./music
VITE_USE_MOCK=false # true = force mock data in dashboard, false = real APIs
Dashboard API Configuration
The dashboard reads hosts from environment at build time:
| Variable | Default | Description |
|---|---|---|
VITE_SNAPCAST_HOST |
localhost |
Snapserver hostname |
VITE_SIGNALK_HOST |
localhost |
SignalK hostname |
VITE_MOPIDY_HOST |
localhost |
Mopidy hostname |
VITE_JELLYFIN_HOST |
localhost |
Jellyfin hostname |
VITE_USE_MOCK |
false |
Force mock data (no real APIs) |
Architecture: Named Pipes
In boot mode, audio flows through named pipes on a tmpfs volume:
spotify.pcm ← librespot (Spotify Connect)
airplay.pcm ← shairport (AirPlay 2)
mopidy.pcm ← mopidy (local files, radio)
Snapserver reads all three as streams and routes them to zones.
Run make pipes once before make boot to create the pipes (or they are created automatically).
In dev mode, pipes are not used (no audio hardware crosses the Docker VM boundary on Mac).
Deployment to Boat (Ubuntu Server)
This system is designed to run on a boat with an Ubuntu server in kiosk mode.
See detailed guide: KIOSK_SETUP.md
Quick Summary
Changes in docker-compose.yml for production:
- Zone audio output — replace
--player file:filename=nullwith--player alsa --soundcard hw:N,0, uncomment/dev/snddevice - Spotify / AirPlay discovery — set
network_mode: hostforlibrespotandshairport - NMEA hardware — uncomment the
devicesblock undersignalk, configure the connection at http://[boat-ip]:3000 - Video decoding (optional) — uncomment
/dev/driunderjellyfin - Use
make bootinstead ofmake dev
Kiosk Mode (Chromium Fullscreen)
chromium-browser --kiosk http://localhost:8080
See KIOSK_SETUP.md for complete Ubuntu server setup with autostart.
Troubleshooting
Zones stuck restarting:
- Check
docker compose logs zone-salon. With snapclient v0.35+, the server URI must betcp://snapserver(not--host snapserver). - If "No audio player support": snapclient needs
--player file:filename=nullin dev (null player not compiled in the bookworm .deb).
Dashboard crashes with exit 127 / npm not found:
- The local
node:20-alpineimage was overwritten by the production dashboard build. Fix:docker rmi node:20-alpine && make dev. - Root cause prevented by
dashboard/Dockerfile.dev— dev uses its own build context.
Shairport config syntax error:
- Boolean values in
config/shairport.confmust be quoted:"yes"notyes.
Spotify device not visible:
- On Mac, Docker Desktop bridges UDP poorly. Use
make spotify(native librespot on Mac) for dev. - On boat: use
network_mode: hostindocker-compose.yml.
Librespot production build fails:
cargo install librespot(latest = v0.8.0) has a vergen_lib dependency conflict.- Fixed by pinning to
--version "=0.5.0"indocker/librespot/Dockerfile.
Snapcast arm64 .deb not found:
- v0.27.0 has no arm64 release. Use v0.35.0 which ships
arm64_bookworm.deb.
Port conflicts (e.g. 8080 taken by Supabase):
- Dev dashboard is on 8090, production on 8080. Change port mappings in
docker-compose.dev.ymlas needed.
Project Layout
.
├── Makefile
├── docker-compose.yml # production stack
├── docker-compose.dev.yml # dev overrides (Vite HMR, null zones, SignalK demo)
├── config/
│ ├── snapserver.conf # pipe sources, HTTP API config
│ ├── mopidy.conf # music backends, HTTP server
│ └── shairport.conf # AirPlay name, pipe output
├── docker/
│ ├── snapserver/Dockerfile # snapcast v0.35.0 from GitHub releases
│ ├── snapclient/Dockerfile # snapcast v0.35.0 from GitHub releases
│ ├── mopidy/Dockerfile # mopidy + iris + local + tunein
│ └── librespot/
│ ├── Dockerfile # production: cargo install librespot v0.5.0
│ └── Dockerfile.dev # dev stub: alpine + sleep
├── dashboard/
│ ├── Dockerfile # production: Vite build → nginx
│ ├── Dockerfile.dev # dev: node:20-alpine base (avoid image overwrite)
│ ├── nginx.conf
│ └── src/
│ ├── api/ # real API clients (snapcast, signalk, mopidy, jellyfin)
│ ├── mock/ # mock data (VITE_USE_MOCK=true)
│ ├── hooks/ # useNMEA, useZones, usePlayer, useDocker
│ └── components/
│ ├── layout/ # TopBar, TabNav
│ ├── audio/ # ZoneCard, ZoneGrid, NowPlaying, SourcePicker
│ └── nav/ # InstrumentPanel, ChartPlaceholder
├── scripts/
│ ├── init-pipes.sh # create named pipes in /tmp/audio
│ ├── setup-dev.sh
│ └── setup-boot.sh
└── music/ # drop audio files here (Mopidy + Jellyfin)