Skip to content

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:4533 only and is reached at https://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 old http://z2mini:4533 / http://100.67.235.68:4533 direct 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:

  1. Version pinned via ${NAVIDROME_VERSION} from .env (not :latest)
  2. Port published on 127.0.0.1 only ("127.0.0.1:4533:4533") — Navidrome is fronted by Caddy at https://navidrome.z2mini.gabrielgabrie.com (see 17-caddy.md)
  3. Music bind mount path at /data/music (separate from container dir, for Samba write access)
  4. TZ set explicitly to America/Toronto
  5. ND_LOGLEVEL: info set 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\music from Windows
  • smb://z2mini/music from 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):

  1. Download the Windows installer from the GitHub releases page
  2. Run, complete first-launch wizard
  3. 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

docker compose logs -f --tail 50 navidrome

Disk usage

du -sh /data/docker/navidrome/data/ /data/music/

Trigger a manual scan

Web UI Settings → "Scan Library Now," or via the Subsonic API:

curl -sS "http://127.0.0.1:4533/rest/startScan.view?u=admin&p=PASS&v=1.16.1&c=cli&f=json"

(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:

  1. Acquire files (iTunes / Bandcamp / CD rip / yt-dlp) on laptop.
  2. Drop into a staging folder on the laptop.
  3. Open Picard → drag files in → Scan → Save.
  4. Drop the renamed/tagged folder into \\z2mini\music\.
  5. 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 via backup-offsite.sh.
  • navidrome.db (the SQLite DB — playlists, play counts, smart playlists, listening history) — captured via a host sqlite3 "<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 live navidrome.db (or its -wal / -shm) — you get a corrupt snapshot. Also goes off-site (it's in db-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:
    docker compose logs --tail 30 navidrome | grep -i scan
    
  • Check ownership: should be gabriel:gabriel (the Samba [music] share has force 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 LYRICS or UNSYNCEDLYRICS tags.
  • Or place .lrc files 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:4533 or https://navidrome.z2mini.gabrielgabrie.com from 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