Self-Hosting Media: Navidrome, Audiobookshelf, and the NFS Bridge to Unraid

Music and audiobooks served from Kubernetes, backed by NFS mounts to an Unraid NAS. Here's the architecture for keeping media libraries on spinning rust while running apps on NVMe.

Self-Hosting Media: Navidrome, Audiobookshelf, and the NFS Bridge to Unraid

When I migrated my homelab from Docker Compose to Kubernetes, the media apps were the ones that made me think hardest about storage. Every other service (Ghost, Miniflux, Life Hub) stores a database measured in megabytes. Navidrome and Audiobookshelf serve media libraries measured in hundreds of gigabytes.

Putting 500 GB of FLAC files on NVMe-backed Longhorn volumes would be wasteful, expensive, and pointless. Music files don't need millisecond I/O latency. They need reliable sequential reads at a modest throughput. That's exactly what a NAS with spinning disks does well.

The solution: NFS. The Kubernetes pods run on NVMe (fast, replicated, local). The media libraries live on Unraid (large, cheap, backed up). NFS bridges the two worlds.

The Architecture

The storage strategy follows a simple rule: NVMe for databases, NAS for media.

Data TypeStorageWhy
App databases, configsLonghorn (NVMe)Fast random I/O, replicated across nodes
Music library (FLAC/MP3)Unraid NFSSequential reads, 500+ GB, cheap storage
Audiobook libraryUnraid NFSSame: large files, sequential reads
BackupsUnraid NFSOff-cluster, survives node failure

Each app gets two volume mounts: a small Longhorn PVC for its database and configuration (fast, replicated) and a large NFS PV for the media library (read-only, shared).

NFS Persistent Volumes

The NFS setup uses static PersistentVolumes: no dynamic provisioner, no storage class. The PVs are defined once in the infrastructure layer and bound by name.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-music
spec:
  capacity:
    storage: 500Gi
  accessModes:
    - ReadOnlyMany
  nfs:
    server: 192.168.1.133
    path: /mnt/user/music
  persistentVolumeReclaimPolicy: Retain
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-audiobooks
spec:
  capacity:
    storage: 500Gi
  accessModes:
    - ReadOnlyMany
  nfs:
    server: 192.168.1.133
    path: /mnt/user/audiobooks
  persistentVolumeReclaimPolicy: Retain

Key decisions:

  • ReadOnlyMany: Both PVs are mounted read-only. The Kubernetes pods consume media but never modify it. New music gets added to the NAS directly, via Samba, SSH, or the Unraid UI. This prevents accidental corruption and means multiple pods can mount the same volume simultaneously.
  • Static binding: PVCs reference the PV by volumeName, not by storage class. This is explicit and predictable. No dynamic provisioner needed, no race conditions on binding.
  • Retain policy: If the PVC is deleted, the PV (and the NFS data) survives. Media libraries are managed outside Kubernetes.

Navidrome is a self-hosted music server that implements the Subsonic API (for mobile apps like DSub and Symphonium) and has its own web UI for browser-based listening.

containers:
- name: navidrome
  image: deluan/navidrome:0.53.3
  ports:
  - containerPort: 4533
  env:
  - name: ND_MUSICFOLDER
    value: /music
  - name: ND_DATAFOLDER
    value: /data
  - name: ND_REVERSEPROXYUSERHEADER
    value: X-authentik-username
  - name: ND_ENABLETRANSCODINGCONFIG
    value: "true"
  volumeMounts:
  - name: data
    mountPath: /data       # Longhorn PVC: database + config
  - name: music
    mountPath: /music      # NFS PV: FLAC/MP3 library
    readOnly: true

The two-volume pattern in action: /data is a 5 GB Longhorn PVC that holds the SQLite database (artist metadata, playlists, user preferences, scrobble history). /music is the 500 GB NFS mount pointing at the Unraid share.

Authentication via ForwardAuth: The ND_REVERSEPROXYUSERHEADER setting enables auto-creation of Navidrome users from the Authentik proxy header. When a user authenticates through Authentik SSO, Traefik's ForwardAuth middleware passes the username, and Navidrome creates a local account automatically. No separate login required.

Transcoding: Navidrome can transcode FLAC to MP3/Opus on the fly for mobile clients on limited bandwidth. This is CPU-bound work that happens in the pod, not on the NAS. The NAS just serves the original files.

The Subsonic API compatibility is what makes this work for mobile. Apps like DSub (Android) and Symphonium connect to Navidrome's Subsonic endpoint through Cloudflare Tunnel, with Authentik handling authentication. The music never touches a third-party server.

Audiobookshelf: Audiobook Library

Audiobookshelf serves audiobooks and podcasts with per-user progress tracking, chapter navigation, and playback speed control. The mobile app syncs position across devices.

containers:
- name: audiobookshelf
  image: ghcr.io/advplyr/audiobookshelf:2.19.4
  ports:
  - containerPort: 80
  volumeMounts:
  - name: config
    mountPath: /config      # Longhorn PVC: database + user data
  - name: metadata
    mountPath: /metadata    # emptyDir: ephemeral, recomputed per pod
  - name: audiobooks
    mountPath: /audiobooks  # NFS PV: audiobook library
    readOnly: true

Same two-volume pattern, plus a third: /metadata is an emptyDir volume for cached cover art and metadata that Audiobookshelf generates on startup. It's ephemeral by design: if the pod restarts, Audiobookshelf rescans and regenerates it. No point in persisting computed data.

The readOnly: true mount on the audiobook library is particularly important here. Audiobookshelf has built-in library management features (rename, move, delete) that could modify source files. Read-only NFS prevents this at the storage layer, regardless of app-level permissions. The NAS is the source of truth; Kubernetes pods are consumers.

Security Hardening

Both deployments follow the same security baseline:

securityContext:
  runAsNonRoot: true
  runAsUser: 1000         # or 33 for Navidrome (www-data)
  allowPrivilegeEscalation: false
  capabilities:
    drop: [ALL]
  readOnlyRootFilesystem: false  # Both apps need to write temp files

Non-root execution, dropped capabilities, no privilege escalation. The readOnlyRootFilesystem is the one compromise: both apps write temporary files during transcoding or metadata processing. A future improvement would be mapping those temp paths to emptyDir volumes and enabling read-only root.

Network policies restrict egress to DNS only. Neither app needs to reach the internet: they serve content from local NFS mounts to local clients through the reverse proxy.

How It Feeds the Data Pipeline

Both Navidrome and Audiobookshelf have REST APIs that Life Hub's collector pipeline taps into for activity tracking.

The Navidrome collector authenticates via JWT and fetches the 50 most recently played songs. The Audiobookshelf collector uses a static bearer token and pulls listening sessions with timestamps, duration, and position data. Both push their records to Life Hub's ingest API every 15 minutes.

Life Hub reaches these services via cluster-internal DNS:

NAVIDROME_URL=http://navidrome.navidrome.svc.cluster.local:80
AUDIOBOOKSHELF_URL=http://audiobookshelf.audiobookshelf.svc.cluster.local:80

No Cloudflare Tunnel, no Authentik, no TLS overhead. Service-to-service communication stays inside the cluster. The collectors see the same API as the mobile apps, just without the authentication middleware.

Why Not Just Run Plex?

Plex is the obvious comparison. It does music, audiobooks, movies, TV, and photos in one app. Three reasons I chose separate, purpose-built tools:

  1. No cloud dependency. Plex routes authentication through their servers. If Plex's auth service goes down (and it has), you can't access your own media. Navidrome and Audiobookshelf authenticate locally.
  2. Subsonic API ecosystem. The Subsonic protocol has decades of client app support. DSub, Symphonium, Ultrasonic, play:Sub. The app choice is enormous. Plex locks you into their client ecosystem.
  3. Resource efficiency. Navidrome uses ~50 MB of RAM at idle. Audiobookshelf uses ~100 MB. Plex's media scanner alone uses more than both combined. On a homelab with constrained resources, this matters.

The NFS Trade-offs

NFS isn't perfect. Here's what I've learned running it:

Startup latency. When Navidrome starts, it scans the music library. Over NFS, this takes longer than local storage: about 2-3 minutes for a full scan versus 30 seconds on local NVMe. But it only happens on pod restart, not during normal operation.

No file locking. NFS v3 (what Unraid serves by default) has limited file locking. This is fine for read-only media mounts but would be dangerous for databases. Never put SQLite on NFS. The Longhorn PVCs handle all database workloads.

Network dependency. If the NAS goes offline, the NFS mounts hang. Kubernetes will mark the pods as unhealthy, but they won't crash cleanly. They'll hang on I/O wait. Adding NFS mount options like soft,timeo=30 would improve this, but I haven't hit it in practice.

No encryption in transit. NFS v3 sends data unencrypted over the LAN. For a trusted home network serving music files, this is an acceptable trade-off. If this were sensitive data or a shared network, NFS v4 with Kerberos or a WireGuard tunnel would be necessary.

Lessons Learned

Separate storage by access pattern. Databases need fast random I/O. Media needs cheap sequential reads. Putting them on the same storage class optimizes for neither. The NVMe/NFS split costs nothing extra and matches each workload to its ideal storage.

Read-only mounts are worth the constraint. Mounting media read-only prevents an entire class of accidents. If an app has a bug that deletes files, or if I misconfigure a library path, the worst case is a pod crash, not data loss.

Static PVs are fine at homelab scale. Dynamic provisioning with a CSI driver is great for clusters with hundreds of volumes. For a homelab with 3-4 NFS shares, static PVs are simpler, more explicit, and easier to debug.

Cluster-internal service discovery eliminates auth overhead. Life Hub's collectors talk to Navidrome and Audiobookshelf over internal DNS. No TLS termination, no SSO middleware, no Cloudflare Tunnel. The security boundary is the namespace network policy, not the application authentication layer.

The whole setup serves my music and audiobook library to any device in my house (and remotely via Cloudflare Tunnel), feeds listening data into my personal analytics dashboard, and runs on hardware I already owned. The NFS bridge to Unraid is what makes it work: it lets Kubernetes do what it's good at (orchestrating applications) while the NAS does what it's good at (serving large files cheaply).