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. Remote browser access stays on the existing Cloudflare Tunnel. The in-cluster origin is still navidrome.navidrome.svc.cluster.local:4533. A temporary MetalLB VIP on the home LAN is an extra path for apps that cannot complete an Access challenge.
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 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.
| Piece | Where | Role |
|---|---|---|
| Application CR | clusters/<cluster>/argocd/applications/navidrome.yaml | Multi-source app |
| Public values | homelabs/navidrome/values.yaml | ClusterIP :4533, claim names, Recreate, metrics on |
| Private overlay | clusters/<cluster>/overlays/navidrome/values.yaml | Temporary service.main LoadBalancer :4533 + MetalLB VIP; Cloudflare still uses in-cluster DNS |
| Data PVC | homelabs/navidrome/manifests/pvc-data.yaml | navidrome-data-pvc-lh |
| Music PV/PVC | clusters/<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. Put LoadBalancer type and MetalLB annotations under service.main only.
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.yamlDay-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:
| Field | Value |
|---|---|
| Subdomain | navidrome (published as navidrome.<your-domain>) |
| Type | HTTP |
| URL | http://navidrome.navidrome.svc.cluster.local:4533 |
| Cloudflare Access | On |
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. Changing the Service to LoadBalancer does not change this URL; the ClusterIP name still answers inside the cluster.
Temporary LAN#
The private overlay may temporarily expose service.main as a MetalLB LoadBalancer on port 4533 (annotation metallb.universe.tf/loadBalancerIPs set to a VIP from your pool, for example <metallb-vip>). LAN clients can then open http://<metallb-vip>:4533 without Cloudflare Access. Treat that VIP as disposable. Keep the Public Hostname and Access policy; do not turn Access off to satisfy a Subsonic app. When Cloudflare Tunnel is enough, return the overlay to ClusterIP :4533 and drop the MetalLB annotation.
Ensure MetalLB first-pool (or your equivalent) includes the chosen VIP before the Application syncs a LoadBalancer, and do not DHCP-reserve MetalLB addresses.
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.
- Sync Application
navidromeand confirm Servicenavidromehas Ready endpoints. For first-run, prefer a port-forward so the wizard is not on a LAN VIP. There is no Public Hostname yet. - Forward the Service and open it on your workstation:
kubectl -n navidrome port-forward svc/navidrome 4533:4533- Create the first admin.
- 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 claim navidrome-music-pvc in namespace navidrome (nfs-csi, ReadWriteMany, 1Ti). The private PersistentVolume uses the same parent NAS export Jellyfin already uses. Public Helm values mount that claim once at /media with readOnly: true. There is no GitOps pin of a subtree such as /media/music, and public values do not set ND_MUSICFOLDER. The chart default leaves Library 1 at /music (required by Navidrome, not editable in the UI, and unused here because nothing is mounted at that path). After first-admin, add real libraries under Settings → Libraries with absolute paths under /media/... (for example /media/music/<artist-or-playlist>/...). 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 jellyfin-media-pvc, and you do not run Navidrome in that namespace. 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 jellyfin get pvc jellyfin-media-pvcnavidrome-data-pvc-lh should be Bound on longhorn-ssd, navidrome-music-pvc Bound on nfs-csi, and the Jellyfin media claim still Bound. No container in navidrome should mount jellyfin-media-pvc.
If a UI library path is rejected or a scan is empty, fix NAS ownership or squash so UID 1000 can read, and confirm the path exists in the pod (MSYS_NO_PATHCONV=1 when using Git Bash on Windows). 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 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 servicemonitorWhile the temporary LAN overlay is in place, Service navidrome is LoadBalancer on port 4533 with EXTERNAL-IP set to your chosen <metallb-vip>. The Cloudflare origin remains http://navidrome.navidrome.svc.cluster.local:4533. A ServiceMonitor named navidrome should exist; Prometheus should scrape /metrics on that Service.
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>, open Settings → Libraries, add a path under /media/... (not Library 1 at /music), and confirm the library lists tracks after scan. 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 Jellyfin and cloudflared alone.