Skip to main content

Navidrome

Navidrome is the household music streamer. It is installed only by Argo CD Application navidrome, which pins community Helm chart navidrome at 6.8.4 (image deluan/navidrome:0.63.1) into namespace navidrome. There is no official chart; the pin is djjudas21/navidrome from https://djjudas21.github.io/charts/. The GitOps file shape matches Jellyfin: public companion manifests, public values, a private overlay, and the upstream chart. Plex stays a LAN LoadBalancer; Navidrome keeps a ClusterIP on port 4533 and is reached the way Jellyfin is reached, through the existing Cloudflare Tunnel.

This page sits with the other application guides. Site hostnames and NAS coordinates stay in homelabs-private/clusters/<cluster>/.

GitOps
#

Application navidrome is multi-source. Public homelabs/navidrome/manifests apply the namespace and the Longhorn data claim. Private clusters/<cluster>/manifests/navidrome apply the NFS music volume. Helm merges $homelabs/navidrome/values.yaml with $private/clusters/<cluster>/overlays/navidrome/values.yaml onto chart navidrome 6.8.4. Sync-wave is 1; CreateNamespace is on.

PieceWhereRole
Application CRclusters/<cluster>/argocd/applications/navidrome.yamlMulti-source app
Public valueshomelabs/navidrome/values.yamlClusterIP :4533, claim names, Recreate, metrics on
Private overlayclusters/<cluster>/overlays/navidrome/values.yamlRepeats service.main ClusterIP :4533 only
Data PVChomelabs/navidrome/manifests/pvc-data.yamlnavidrome-data-pvc-lh
Music PV/PVCclusters/<cluster>/manifests/navidrome/navidrome-music-pvc on nfs-csi

The chart uses the bjw-s common dialect: global.fullnameOverride, controller.strategy: Recreate, and service.main for the Service. Do not copy Jellyfin keys service.type or service.port into these values; they crash helm template on this chart. The overlay repeats only service.main ClusterIP on :4533; it never sets LoadBalancer, MetalLB, or those Jellyfin keys.

Public values also set UID/GID 1000, drop all capabilities, automountServiceAccountToken: false, sharing off (ND_ENABLESHARING), transcode-config UI off (ND_ENABLETRANSCODINGCONFIG), and ND_ENFORCENONROOTUSER: "true". They request 100m CPU / 256Mi memory and limit 1 CPU / 1Gi.

Native Prometheus /metrics is on via ND_PROMETHEUS_ENABLED and service.main.monitor.enabled. kube-prometheus-stack already scrapes every ServiceMonitor, so no extra Prometheus install is required. Do not add a Public Hostname whose only purpose is /metrics. There is no Grafana dashboard or PrometheusRule for Navidrome in v1.

AppProject homelab must allow the chart repo https://djjudas21.github.io/charts/ and destination namespace navidrome. Apply the live project before the Application is committed:

kubectl apply -f homelabs-private/argocd/projects/homelab.yaml

Day-to-day changes go through Git and an Argo sync. Do not helm install Navidrome from a workstation. Pin stays 6.8.4; treat a chart or appVersion bump as a new change.

Public hostname
#

The connector is the existing cloudflared Deployment in namespace cloudflare-tunnel. You do not add a second tunnel, and you do not edit homelabs/cloudflare/. In Zero Trust, add a Public Hostname on that tunnel:

FieldValue
Subdomainnavidrome (published as navidrome.<your-domain>)
TypeHTTP
URLhttp://navidrome.navidrome.svc.cluster.local:4533
Cloudflare AccessOn

Put Access in front of this hostname before it answers. Use an email allow-list or an existing household identity group, not Everyone, and not an empty policy. Dex and Grafana stay Access-off because they are an identity issuer and its callback; Navidrome is neither, so Access stays on after first-run as well. Subsonic and official apps that cannot complete an Access challenge are a later review, not a reason to turn Access off here. Household clients such as Amperfy on iOS use the Navidrome Subsonic URL and a native user you create after first-admin.

Dashboard route changes apply without restarting cloudflared. The connector itself is documented under Cloudflare Tunnel.

First-run admin
#

Anyone who can hit :4533 before an admin exists becomes the owner. Create the first native admin on a path that is not public, then publish the hostname with Access already attached.

  1. Sync Application navidrome and confirm Service navidrome is ClusterIP with Ready endpoints and no EXTERNAL-IP. There is no Public Hostname yet.
  2. Forward the Service and open it on your workstation:
kubectl -n navidrome port-forward svc/navidrome 4533:4533
  1. Create the first admin.
  2. Then create the Public Hostname above, with Access already on.

Do not run first-admin on a world-open origin, and do not create the hostname before Access exists.

There is no ExternalSecret, Vault path, Dex client, or reverse-proxy ExtAuth in v1. Access at the Cloudflare edge is the identity gate; the admin password lives in /data on the Longhorn claim after first-user create.

Storage
#

Data state is claim navidrome-data-pvc-lh: longhorn-ssd, ReadWriteOnce, 2Gi, GitOps from homelabs/navidrome/manifests/pvc-data.yaml and Helm persistence.config.existingClaim at /data. SQLite, image cache, and transcode cache live here, not on the NAS. The Deployment strategy is Recreate so that single RWO volume can remount after a replace.

Music is a new claim navidrome-music-pvc in namespace navidrome (nfs-csi, ReadWriteMany, 1Ti). The private PersistentVolume uses the same parent NAS export Plex and Jellyfin already use. Helm mounts that claim at /media with readOnly: true, and ND_MUSICFOLDER is /media/music so Navidrome indexes only the existing music tree. Do not point the PV at a nested NFS path, and do not add a Helm subPath: the NAS exports the parent share only, and kubelet cannot create a subPath directory on a read-only volume. You do not remount plex-media-pvc or jellyfin-media-pvc, and you do not run Navidrome in those namespaces. NAS server and share belong only in clusters/<cluster>/manifests/navidrome/, not in public git and not on this page.

Confirm the claims after sync:

kubectl -n navidrome get pvc
kubectl -n plex get pvc plex-media-pvc
kubectl -n jellyfin get pvc jellyfin-media-pvc

navidrome-data-pvc-lh should be Bound on longhorn-ssd, navidrome-music-pvc Bound on nfs-csi, and the Plex and Jellyfin media claims still Bound. No container in navidrome should mount plex-media-pvc or jellyfin-media-pvc.

If the pod cannot read /media/music, fix NAS ownership or squash so UID 1000 can read. The subdirectory name on the export must match ND_MUSICFOLDER; list /media in the pod if a scan is empty. Do not run the container as root to paper over permissions.

Deploy
#

Commit order is homelabs (feature branch + PR so navidrome/ exists on main) → homelabs-private on main (AppProject apply first, then overlay, music volume, Application) → this docs site. Parent app-of-apps already watches argocd/applications/.

After sync, the Application should be Synced/Healthy, the Service ClusterIP only, and the pod Ready:

kubectl -n argocd get application navidrome
kubectl -n navidrome get svc navidrome
kubectl -n navidrome get pods
kubectl -n navidrome get servicemonitor

Service navidrome must show type ClusterIP on port 4533 and must not have an EXTERNAL-IP. A ServiceMonitor named navidrome should exist; Prometheus should scrape /metrics on that ClusterIP.

Smoke
#

v1 health is pod Ready plus a Prometheus target up for the Navidrome ServiceMonitor. After the hostname exists, sign in through Access at https://navidrome.<your-domain> and confirm the library lists tracks from the existing music tree. An Access challenge before that is expected. A Cloudflare 502 means the origin Service or pod (the same signal as Immich and Jellyfin), so check svc/navidrome and the Deployment, not a VIP.

There is no Blackbox Probe for Navidrome in v1. kube-prometheus-stack already watches pod Ready and the Longhorn data volume.

Rollback
#

Disable or delete Application navidrome, or scale the Deployment to 0. Data remains on navidrome-data-pvc-lh until you delete that claim. Music files stay on the NAS (Retain). Remove the Zero Trust Public Hostname and its Access application. Leave Plex, Jellyfin, and cloudflared alone.