12 — Navidrome Music Streaming¶
Self-hosted music server running on the Z2, replacing paid streaming subscriptions for self-curated listening. Tailscale-only; deployed as a single Docker container; Subsonic-API-compatible for a wide ecosystem of clients.
Behind the Caddy reverse proxy (May 2026). Navidrome's container binds
127.0.0.1:4533only and is reached athttps://navidrome.z2mini.gabrielgabrie.com(auto-renewing Let's Encrypt cert via Caddy). Subsonic clients (Narjo on iOS, Feishin on the laptop) — Server URL =https://navidrome.z2mini.gabrielgabrie.com. On the box itself:http://127.0.0.1:4533. The oldhttp://z2mini:4533/http://100.67.235.68:4533direct URLs no longer work — apps aren't reachable on the tailnet directly anymore, only through Caddy.
Overview¶
Single-container Docker stack:
| Container | Image | Purpose |
|---|---|---|
navidrome |
deluan/navidrome:0.61.2 |
Web UI, REST API, Subsonic-compatible API, file watcher, on-the-fly transcoding |
Reachable at https://navidrome.z2mini.gabrielgabrie.com from any tailnet device. Not exposed to the public internet.
The music library lives at /data/music/ — separate from the Navidrome container directory at /data/docker/navidrome/. This separation lets Samba share the music folder for laptop/iOS writes without exposing the Navidrome runtime data, and leaves a future Jellyfin install free to point at the same /data/music/ folder if music ever gets unified with video.
Design decisions¶
| Decision | Reasoning |
|---|---|
| Navidrome over Jellyfin / Plex / Funkwhale | Audio-only purpose-built, lightweight (~50 MB RAM at idle), Subsonic-API ecosystem (many iOS clients with CarPlay), FOSS. Jellyfin viable but heavier; can be added later for video without disturbing Navidrome. Plex required Plex Pass ($120) for offline downloads, violating the no-subscription rule. |
Bound to 127.0.0.1:4533, fronted by Caddy |
Apps live behind the single Caddy ingress at https://navidrome.z2mini.gabrielgabrie.com; never reachable on the tailnet directly or publicly. (Was 100.67.235.68:4533 before the Caddy migration.) See 17-caddy.md. |
/data/music/ separate from /data/docker/navidrome/ |
Music files live independently of the container dir; Samba [music] share points at it for writes; future Jellyfin can point at the same folder |
Music dir mounted read-only inside the container (/data/music:/music:ro) |
Navidrome only reads files. The :ro is the container's view; the host filesystem is unaffected and Samba writes work normally |
Library format mostly AAC 256 (.m4a); FLAC accepted |
iTunes Store delivers AAC 256 natively, native iOS decoding (no transcoding lag), audibly transparent on car/BT-speaker/loudspeaker gear, ~5 MB/track. FLAC from Bandcamp / library CD rips is also supported and decoded natively by modern Subsonic clients. Mixed-format library is fine. |
Image pinning via ${NAVIDROME_VERSION} in .env |
Updates are deliberate (docker compose pull + restart), never automatic. Same posture as Immich. |
restart: unless-stopped (not always) |
Survives reboots, but docker compose stop actually stops |
| iOS client: Narjo | Trialled Arpeggio and Narjo (both free, both CarPlay + native FLAC); Narjo won out and Arpeggio is no longer used. Amperfy needs iOS 26 (not viable on the iPhone's current iOS); play:Sub is paid $4.99 (acceptable but not chosen). |
Evaluated and not chosen as the music server: Jellyfin (overkill for audio-only — may run alongside later for video), Plex (Plex Pass cost), Funkwhale (smaller community, rougher iOS apps), Ampache (older PHP, dated UX), Mopidy (building block, not a finished product).
Install¶
Directory layout¶
/data/docker/navidrome/
├── docker-compose.yml ← stack definition, version-pinned
├── .env ← version pin
└── data/ ← Navidrome's SQLite DB, cache, thumbnails (uid 1000)
/data/music/ ← music files (separate from container dir)
read-only inside container, writable from Samba
docker-compose.yml¶
Mirrors the upstream Navidrome template verbatim except for these intentional customizations:
- Version pinned via
${NAVIDROME_VERSION}from.env(not:latest) - Port published on
127.0.0.1only ("127.0.0.1:4533:4533") — Navidrome is fronted by Caddy athttps://navidrome.z2mini.gabrielgabrie.com(see 17-caddy.md) - Music bind mount path at
/data/music(separate from container dir, for Samba write access) TZset explicitly to America/TorontoND_LOGLEVEL: infoset explicitly (default; spelled out for clarity)
name: navidrome
services:
navidrome:
container_name: navidrome
image: deluan/navidrome:${NAVIDROME_VERSION}
user: "1000:1000"
ports:
- "127.0.0.1:4533:4533"
restart: unless-stopped
environment:
ND_LOGLEVEL: info
TZ: America/Toronto
volumes:
- "/data/docker/navidrome/data:/data"
- "/data/music:/music:ro"
.env¶
# Version pin — verify current stable:
# curl -s https://api.github.com/repos/navidrome/navidrome/releases/latest | grep tag_name
NAVIDROME_VERSION=0.61.2
First boot¶
mkdir -p /data/docker/navidrome/data /data/music
cd /data/docker/navidrome
# Write docker-compose.yml + .env (see above)
docker compose config --quiet # validate YAML + variable resolution
docker compose pull # ~330 MB image
docker compose up -d
docker compose ps # confirm "Up X seconds"
HTTP smoke test (auth fails as expected — proves the API is responding):
curl -sS 'http://127.0.0.1:4533/rest/ping.view?u=test&p=test&v=1.16.1&c=test&f=json'
# → {"subsonic-response":{"status":"failed","version":"1.16.1","type":"navidrome",
# "serverVersion":"0.61.2 (...)","openSubsonic":true,
# "error":{"code":40,"message":"Wrong username or password"}}}
First-run admin account¶
Open https://navidrome.z2mini.gabrielgabrie.com in your browser. Set the admin username and password (separate from any other credentials — Navidrome has its own user database).
Music library writes — Samba [music] share¶
To allow drag-and-drop writes from Windows / iOS Files / macOS Finder into /data/music/, an additional Samba share [music] was added to /etc/samba/smb.conf mirroring the existing [files] share pattern. See 04-samba.md for the share definition and Samba-side details.
Reachable at:
\\z2mini\musicfrom Windowssmb://z2mini/musicfrom macOS Finder or iOS Files
Same Samba credentials as \\z2mini\files.
Navidrome's built-in file watcher detects new files and triggers a partial scan within ~10 seconds. No manual refresh needed in normal operation.
iOS app setup¶
Subsonic-API clients connect to Navidrome. Narjo is the one in active use (Arpeggio was trialled and dropped); the table below covers the realistic options.
| App | Cost | Notes |
|---|---|---|
| Narjo | Free (TestFlight beta) | In active use; production version may differ when it ships on the App Store |
| Arpeggio | Free | Trialled, no longer used (Narjo won out) |
| Amperfy | Free, FOSS | Recent versions require iOS 26 — not viable on older iOS |
| play:Sub | $4.99 one-time | Most polished commercial Subsonic client |
| substreamer | Free with optional IAP | Solid alternative |
Connection settings are identical across apps:
| Field | Value |
|---|---|
| Server URL | https://navidrome.z2mini.gabrielgabrie.com |
| Username | (Navidrome admin) |
| Password | (Navidrome admin password) |
CarPlay is supported on Narjo (and Arpeggio, play:Sub, substreamer). Required for the primary use case (commute listening). Pair with the in-app offline download feature for tracks that need to play through cellular dead zones — Navidrome streams over Tailscale, which goes through cellular when away from home Wi-Fi, so dead zones cause stutters unless tracks are pre-cached.
Laptop client¶
Feishin — free, FOSS, cross-platform Subsonic desktop client (Windows / Mac / Linux). Spotify-inspired UI: sidebar nav, big now-playing, album-grid browsing, queue panel. Noticeably better shuffle and queue management than Navidrome's web UI. Optional Last.fm / ListenBrainz scrobbling for play-history tracking.
Created by the dev who made Sonixd; Sonixd was archived in 2023, Feishin is the modern successor.
Setup (~5 minutes):
- Download the Windows installer from the GitHub releases page
- Run, complete first-launch wizard
- Server type: Subsonic; URL:
https://navidrome.z2mini.gabrielgabrie.com; credentials: Navidrome admin
The Navidrome web UI at https://navidrome.z2mini.gabrielgabrie.com is still useful for first-time setup, library administration, and one-off listening from any browser without installing anything. For daily-driver desk listening, Feishin is the right tool — the web UI's shuffle is limited (shuffles within the current view rather than smartly across the library; no radio / smart-shuffle mode).
Other Subsonic desktop clients evaluated:
- Supersonic — native (Go + Fyne, no Electron); lighter on RAM, less polished UI. Reasonable alternative if Electron is a dealbreaker.
- Sonixd — archived 2023; install Feishin instead.
- Foobar2000 with Subsonic plugin (Windows-only) — power-user player with steep learning curve. Not used.
Operations¶
Start / stop / pull updates¶
cd /data/docker/navidrome
docker compose up -d # start (or recreate after pull)
docker compose stop # stop without removing
docker compose down # stop and remove (data preserved)
# Updates: bump NAVIDROME_VERSION in .env, then:
docker compose pull
docker compose up -d
Logs¶
Disk usage¶
Trigger a manual scan¶
Web UI Settings → "Scan Library Now," or via the Subsonic API:
(Normal operation: the file watcher triggers scans automatically. Manual scan is only needed if files are added by methods that bypass the watcher — e.g., in-place changes to a file's tags via SSH.)
Connecting from on the server itself¶
Navidrome binds 127.0.0.1:4533, so scripts/tools on z2mini use http://127.0.0.1:4533 (or http://localhost:4533). https://navidrome.z2mini.gabrielgabrie.com also works from the box (via Caddy). http://z2mini:4533 and http://100.67.235.68:4533 no longer work — those bindings were removed when Navidrome moved behind Caddy. From other tailnet devices, the only way in is https://navidrome.z2mini.gabrielgabrie.com.
Tagging workflow¶
Navidrome reads metadata from embedded tags inside each audio file (ID3 for MP3, MP4 atoms for AAC/M4A, Vorbis comments for FLAC). Folder structure and filename are cosmetic — tags drive the library.
Tag quality varies by source:
| Source | Tag quality | Action |
|---|---|---|
| iTunes Store purchase | Excellent — fully tagged, embedded artwork | Drop in directly |
| CD rip via Apple Music app | Good — Gracenote auto-fetched | Usually fine as-is |
| Bandcamp download | Good — artist self-tagged | Usually fine; run through Picard for consistency if desired |
| yt-dlp from YouTube/SoundCloud | Bad — usually only "title" field | Run through MusicBrainz Picard before drop |
| Random sources (USB, friend, old rips) | Variable | Run through Picard |
MusicBrainz Picard (picard.musicbrainz.org) — desktop app, runs on the Windows laptop (not the server, not iOS). Acoustic-fingerprints files against the MusicBrainz database, writes correct tags + (optionally) renames into a configurable template like Artist/Album/01 - Title.m4a. Free, FOSS, two clicks per album.
Workflow:
- Acquire files (iTunes / Bandcamp / CD rip / yt-dlp) on laptop.
- Drop into a staging folder on the laptop.
- Open Picard → drag files in → Scan → Save.
- Drop the renamed/tagged folder into
\\z2mini\music\. - Navidrome's watcher scans and indexes within ~10 seconds.
For iTunes-purchased files, Picard is optional — they're already tagged correctly.
Backup considerations¶
Navidrome is in the nightly backup script (since May 2026 — see 05-backups.md):
/data/music/— rsync-mirrored to/mnt/backup/current/music/every night (with--delete). It's rebuildable from acquisition sources (iTunes purchase history, CD rips, Bandcamp account, etc.) but reacquisition time is real, so it's backed up. Also goes to the off-site T5 viabackup-offsite.sh.navidrome.db(the SQLite DB — playlists, play counts, smart playlists, listening history) — captured via a hostsqlite3 "<live db>" ".backup '<dest>'"into/mnt/backup/current/db-dumps/navidrome.db. A proper online backup, safe while the container holds the WAL-mode DB open. Never raw-rsync the livenavidrome.db(or its-wal/-shm) — you get a corrupt snapshot. Also goes off-site (it's indb-dumps/)..env+docker-compose.yml— rsync'd into/mnt/backup/current/service-config/navidrome/.
What's not backed up: data/artwork/ and data/cache/ — regenerated automatically on the next library scan. (That's why the service-config/navidrome/ rsync excludes the whole data/ dir — the DB is captured separately as a dump, the rest is regenerable.)
Restore: drop db-dumps/navidrome.db into /data/docker/navidrome/data/navidrome.db, restore .env + compose from service-config/navidrome/, rsync the music back, docker compose up -d — artwork/cache rebuild on the first scan. See 08-recovery.md → Step 6b.
Troubleshooting¶
iOS app says "Library not found or empty" after install:
- Cause: literally empty library, no music files yet.
- Fix: drop files into
/data/music/via Samba; watcher scans within ~10 seconds.
Files dropped into /data/music/ don't appear in Navidrome:
- Check the watcher fired:
- Check ownership: should be
gabriel:gabriel(the Samba[music]share hasforce user = gabriel). - If watcher missed: trigger a manual scan via the web UI.
FLAC files play but lyrics don't show:
- Embedded synced lyrics: Navidrome reads Vorbis-comment
LYRICSorUNSYNCEDLYRICStags. - Or place
.lrcfiles alongside the audio (same name, different extension).
Track-start lag / stuttering:
- Cause: transcoding (rare with AAC/MP3/FLAC — these direct-play), network latency over cellular, or weak Wi-Fi.
- Fix: use the iOS app's offline download feature for albums you'll listen to away from home Wi-Fi. The big lever for the commute use case.
http://localhost:4533 doesn't work from the server:
- Same gotcha as Immich — bound to Tailscale interface IP only.
- Fix: use
http://127.0.0.1:4533orhttps://navidrome.z2mini.gabrielgabrie.comfrom on-server scripts.
iOS app version is stale (Narjo TestFlight expired):
- TestFlight builds expire ~90 days. Re-install via TestFlight or switch to a different client (Arpeggio, play:Sub).
See also¶
- 04-samba.md — Samba
[music]share for laptop / iOS writes into/data/music/ - 05-backups.md — the nightly backup:
/data/music/is mirrored andnavidrome.dbis captured viasqlite3 .backup - 08-recovery.md — disaster recovery, including the per-service restore (Step 6b)
- 10-system-reference.md — quick lookup for paths, ports, services
- 11-immich.md — Photo storage; same Docker stack pattern
- z2mini-context-for-ai.md — AI-context document, kept in sync with this page