Skip to content

RFC: Self-Hosted Music Streaming (Replacing YouTube Music)

Status: Draft Date: 2026-04-24

Problem

15 years on YouTube Music. Solid local music collection exists but no self-hosted way to stream it with discovery, recommendations, iOS app, CarPlay, offline sync, and the other features that make YouTube Music sticky.

Requirements

  1. Stream local music collection from the homelab
  2. iOS app with CarPlay, gapless playback, offline downloads
  3. Music discovery / recommendations (the hardest gap to fill)
  4. Lyrics (synced preferred)
  5. Scrobbling (Last.fm / ListenBrainz)
  6. Lightweight — runs in a Debian LXC alongside the existing stack
  7. Fits the IaC pattern: Ansible + Forgejo Actions + Vault + Caddy + Alloy

Nice-to-haves

  • OIDC/SSO via PocketID
  • Smart playlists
  • YouTube Music bridge (for music not in local collection)
  • Multi-user

Server Comparison

Tier 1 — Serious Contenders

Navidrome Jellyfin (music mode) Gonic
Language Go C# Go
GitHub stars 20.7k huge 2.4k
Latest release v0.61.2 (2026-04-12) active v0.20.1 (2026-01-30)
RAM footprint ~50MB ~300-500MB ~50MB
Subsonic API Full (OpenSubsonic) No (own API) Full
OIDC/SSO No (rejected, #858) Plugin (alpha, tested w/ PocketID) No
Smart playlists Yes (iTunes-style rules) No (for music) No
Scrobbling Last.fm + ListenBrainz + Maloja Via plugins Last.fm + ListenBrainz
Lyrics Embedded + .lrc .lrc files No
Recommendations No native AudioMuse-AI plugin No
Artist metadata Last.fm / Spotify / Deezer MusicBrainz / fanart.tv No
Transcoding On-the-fly, per-user FFmpeg (full) FFmpeg
Multi-user Yes Yes Yes
Deployment Single binary or Docker Docker or bare metal Single binary or Docker
Server Why not
Koel (17.1k stars) PHP/Laravel stack, SSO locked behind premium, no Subsonic API
Funkwhale Federation overkill, Python/Django + PostgreSQL + Redis + Celery = heavy
Ampache (3.8k stars) Aging PHP, lead dev on reduced hours, uncertain future
Airsonic-Advanced Last stable release April 2020, Java/JVM 500MB+ RAM
Polaris (2.6k stars, Rust) No Subsonic API, no scrobbling, no lyrics — too basic
Black Candy (4.2k stars, Ruby) No Subsonic API, no smart playlists, limited features
Euterpe (572 stars, Go) Extremely minimal, no scrobbling/playlists/lyrics
Supysonic (289 stars, Python) Bare-bones, just use Navidrome instead
mStream (Node.js) Basic personal streamer, not competitive
LMS/Lyrion (Perl) Great for Squeezebox hardware, wrong fit for mobile streaming

iOS App Comparison

Subsonic-Compatible (works with Navidrome / Gonic)

App Price CarPlay Gapless Offline Scrobbling Last Updated Status
play:Sub $4.99 Yes (w/ queue) Yes Yes Last.fm 2026-04-22 Best overall, mature
Arpeggi Free (OSS) Yes Yes (AAC/FLAC/ALAC/Opus) Yes (bulk) Yes (offline queue) v0.6.4 beta TestFlight only, at capacity
Narjo Free (beta) Yes Yes Yes Unknown TestFlight beta Unique: Subsonic + Emby, DLNA/Sonos
Amperfy Free (OSS) Yes No (micro-gaps FLAC) Yes Yes 2025-03 Stable, no Opus support
SubStreamer Free Unknown Unknown Yes Analytics 2026-04-20 Active
iSub Free No Yes Yes No 2021-03 Abandoned

Jellyfin-Only

App Price CarPlay Gapless Offline Last Updated Status
Finamp Free (OSS) Not yet Yes Yes 2024-11 Active redesign

Key Takeaways

  • play:Sub is the safe pick — mature, CarPlay, everything works, $5
  • Arpeggi is the most promising free option — true gapless on all formats including Opus, open source, but beta-only and TestFlight full
  • Narjo is interesting if you have Sonos/DLNA speakers or want Emby support alongside Navidrome
  • Amperfy is solid but the FLAC gapless gap and no Opus support are real limitations
  • Jellyfin locks you to Finamp only — no CarPlay yet, smaller ecosystem

YouTube Music Gap Analysis

What you lose and how to mitigate

Feature YouTube Music Self-Hosted Mitigation
Discovery / recs Algorithm-driven mixes ListenBrainz collaborative filtering + Navidrome smart playlists
Radio from song "Start Radio" Subsonic "Similar Songs" API (works in play:Sub/Amperfy)
Synced lyrics Built-in Embedded tags + .lrc files (batch via LRCLIB)
Artist bios Rich pages Navidrome auto-enriches from Last.fm/Spotify/Deezer
Scrobbling YouTube history Last.fm + ListenBrainz (native in Navidrome)
Music you don't own Unlimited catalog See "Acquisition Bridge" below

The Acquisition Bridge: ytmusicapi + Yubal

The biggest gap: YouTube Music has every song ever. Self-hosted only has what you have.

ytmusicapi (v1.11.6) is a Python library that talks to YouTube Music's internal API: - Fetch liked songs, playlists, listening history, recommendations - Search, get artist info, get lyrics - OAuth authentication

Yubal bridges ytmusicapi + yt-dlp + Navidrome: - Paste a YouTube Music album/playlist URL -> downloads, tags, organizes into Artist/Year-Album/Track - Generates M3U playlists + synced .lrc lyrics (via lrclib.net) - Scheduled sync — subscribe to YTM playlists, auto-sync on cron - ReplayGain tagging, deduplication - Browser extension (Firefox + Chrome) for one-click downloads - Docker deployment

NavidroFM automates the discovery loop: - Reads Navidrome scrobbles -> queries YouTube Music for similar music -> downloads + imports - Closest thing to YouTube Music's "Start Radio" but self-hosted

Practical workflow:

Listen on Navidrome -> Scrobble to ListenBrainz -> Get recommendations
-> Yubal/NavidroFM auto-downloads from YouTube Music -> Navidrome rescans
-> New music appears in library -> Repeat

This turns the "I don't have that song" problem into an automated pipeline. ListenBrainz recommends Creed after Linkin Park -> NavidroFM fetches it from YouTube Music -> it appears in your library next scan.


SSO Trade-off

Navidrome explicitly rejected OIDC (GitHub #858). Reverse proxy header auth exists but breaks Subsonic API auth — mobile apps authenticate with username/password tokens directly to the Subsonic API, bypassing the reverse proxy auth layer.

Practical approach: - Caddy reverse proxy for the web UI (restrict via NetBird VPN or IP allowlist) - Strong password set directly in Navidrome - Mobile apps authenticate via Subsonic API tokens (password-based) - This is standard practice in the Navidrome community

If SSO is a hard requirement: Jellyfin + SSO plugin (tested with PocketID) + Finamp. But you lose CarPlay, the Subsonic app ecosystem, and the lightweight footprint.

Recommendation: Accept the SSO gap. Music streaming is a personal-use service where a strong password is sufficient. The trade-off (losing the entire Subsonic mobile ecosystem) isn't worth it for SSO on a single-user music server.


Decision: Navidrome

Why

  1. Lightest footprint — single Go binary, ~50MB RAM, SQLite. Perfect for a Debian LXC
  2. Best mobile ecosystem — OpenSubsonic API unlocks play:Sub, Arpeggi, Amperfy, SubStreamer, and dozens more
  3. Most active development — 20.7k stars, v0.61.2 released 12 days ago
  4. Feature-rich — smart playlists, scrobbling, lyrics, artist metadata, multi-user, transcoding
  5. Simple deployment — Docker, trivial Ansible role, no database server needed
  6. Handles scale — tested with 900k+ songs
  7. Acquisition bridge — Yubal + NavidroFM solve the "music I don't have" problem

Proposed Infrastructure

Component Value
LXC ID 132
IP 192.168.1.132
Hostname navidrome
Storage pool apps-pool
Disk 4G (rootfs only — music on bind mount)
URL music.eva-00.network
Music storage Bind mount from host (location TBD — filedump or unohana)
Containers navidrome (+ yubal optional)

Companion Stack

Component Purpose
Navidrome Music server (OpenSubsonic API)
play:Sub (iOS) Primary mobile client ($4.99)
Arpeggi (iOS) Secondary client (free, beta — try both)
ListenBrainz Open scrobbling + recommendation engine
Last.fm Social scrobbling + metadata enrichment
Yubal (Docker) YouTube Music bridge — download, tag, organize, sync
MusicBrainz Picard Initial library tagging (run on Mac, one-time)
LRCLIB Batch synced lyrics download

Resolved Questions

  1. Music storage: unohana (sdc, 4TB) via bind mount from /mnt/all-might. Same disk as Jellyfin, Grimmory, Shoko media. Music files + Yubal-downloaded .lrc lyrics live here. Navidrome's own data (SQLite DB, cover art cache, metadata) stays in LXC rootfs at /opt/navidrome/data/.
  2. Yubal deployment: Colocated on LXC 132 with Navidrome. Tightly coupled — Yubal writes directly to Navidrome's music directory. Both lightweight.
  3. YouTube Music: Keep active during migration. Yubal pulls the entire YTM library first, then evaluate whether to cancel.
  4. iOS apps: User is in Arpeggi + Narjo betas. Evaluate alongside play:Sub before committing to a purchase. No Sonos/DLNA speakers, so Narjo's unique feature is less relevant.
  5. LXC 132: Dedicated to Navidrome + Yubal. Nothing else runs here.
  6. Backup: Music files on unohana are already covered by sazabi (18TB offline rsync). Navidrome app data (SQLite DB, config, playlists) backed up via PBS like other LXCs. Yubal config backed up alongside.
  7. No SSO: Accept the SSO gap. Strong password in Navidrome, Caddy reverse proxy for web UI access. Mobile apps use Subsonic API tokens directly.

Implementation Plan

Infrastructure

Component Value
LXC ID 132
IP 192.168.1.132
Hostname navidrome
Storage pool apps-pool
Disk 4G (rootfs — music on bind mount)
URL music.eva-00.network
Music bind mount /mnt/all-might -> /music
Containers navidrome (port 4533), yubal (port 8000), alloy

IaC Artifacts (chizuru-v2)

Artifact Path
Inventory ansible/inventory/hosts.yml
Playbook ansible/playbooks/navidrome.yml
Workflow .forgejo/workflows/navidrome.yml
Docker Compose services/navidrome/docker-compose.yml
Navidrome config services/navidrome/navidrome.toml
Caddy entry services/caddy/Caddyfile > music.eva-00.network
Glance entries services/glance/glance.yml > Media bookmarks, monitor, Proxmox, releases

Vault Secrets

Path Keys
secret/navidrome admin_password, lastfm_api_key, lastfm_secret

Glance Icons (selfh.st)

Service URL
Navidrome https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg/navidrome.svg
Yubal https://cdn.jsdelivr.net/gh/selfhst/icons@main/svg/yubal.svg

Documentation (homelab-docs)

File Purpose
docs/services/navidrome/setup.md Setup, IaC, Vault, Yubal, iOS clients
docs/services/navidrome/runbook.md Operations, troubleshooting
mkdocs.yml Nav entry under Media section

Backup Strategy

  • LXC 132 rootfs -> PBS automatic backup (covers SQLite DB, playlists, favorites, cover art cache, Yubal state)
  • Music files on unohana -> Already in sazabi 18TB offline rsync
  • Vault secrets -> Already backed up with Vault storage backend
  • No additional backup config needed

Post-Deploy Manual Steps

  1. Browse https://music.eva-00.network, create admin account with Vault password
  2. Register Last.fm API key at https://www.last.fm/api/account/create, store in Vault
  3. Set up Yubal YouTube Music OAuth (one-time browser auth flow)
  4. Install Yubal browser extension (Firefox/Chrome) for one-click downloads
  5. Connect iOS apps (play:Sub / Arpeggi / Narjo) to music.eva-00.network
  6. Transfer music collection to /mnt/all-might/music/ on chizuru