Self-Hosted Music Streaming on K3s: Deploy Navidrome and Syncthing

Self-Hosted Music Streaming on K3s: Deploy Navidrome and Syncthing

Introduction

The majority of us listen to music while working in the office, at home, or while traveling. Although Spotify, Apple Music, and Google’s YouTube Music are highly popular music streaming applications, each necessitates a paid subscription. It’s also possible that you don’t depend on these subscriptions and have accumulated hundreds of MP3 audio files over the years, which you can only play on your phone by copying them to the local storage. This is simple, but what if you want to keep the same playlist on all of your devices?

Background

I’m not a guy who is into audiophiles or music. While I do listen to various genres of music on YouTube and while on the go, I am not overly concerned with the way I organize my collection. In the past, I was too lazy to download new songs and upload them to my phone, so I would just move them to my local storage and listen to the same playlist nonstop for months. After I began searching for self-hosted applications to install on my K3s cluster, everything changed. I discovered the Navidrome project, which is a self-hosted, open-source music server that can be used to stream and manage your own music library similarly to Spotify. I know this isn’t an essential lifestyle app, but I was in favor of having my own hosting app since I already paid for a virtual private server (VPS) every year.

Prerequisite

  1. A remote VPS, local server, or Network Attached Storage (NAS) to host Navidrome and Syncthing (explained below)
  2. Network access (ideally a public IP with a domain name) to access your music library outside your home
  3. Music files ^.^

Step 1 – Navidrome Kubernetes Manifest

Navidrome v0.58.0 introduces multi-library support, a major architectural enhancement that enables users to organize and manage multiple music collections with proper permission controls and complete UI integration. Imagine having a song library for your children as they grow up, which will continue to exist even after mobile phones have been upgraded. Now, Navidrome does not offer official Helm charts; even if you locate community-maintained Helm charts, they are likely outdated with regard to (see Navidrome Configuration Options). The following is a Kubernetes Manifest YAML script that I employed to deploy Navidrome v0.58.0 in my K3s Kubernetes cluster.

apiVersion: v1
kind: Namespace
metadata:
  name: navidrome
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: navidrome-data-pvc
  namespace: navidrome
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 200Mi
  storageClassName: longhorn
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: navidrome-music-pvc
  namespace: navidrome
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: longhorn
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: navidrome
  namespace: navidrome
  labels:
    app: navidrome
spec:
  replicas: 1
  selector:
    matchLabels:
      app: navidrome
  template:
    metadata:
      labels:
        app: navidrome
    spec:
      containers:
        - name: navidrome
          image: deluan/navidrome:0.58.0
          imagePullPolicy: IfNotPresent
          env:
            - name: ND_MUSICFOLDER
              value: /music
            - name: ND_DATAFOLDER
              value: /data
            - name: ND_LOGLEVEL
              value: info
            - name: ND_SESSIONTIMEOUT
              value: 24h
            - name: ND_SCANNER_SCHEDULE
              value: "@every 15m"
            # Optional new settings in 0.58:
            - name: ND_ENABLETRANSCODINGCONFIG
              value: "true"
            - name: ND_BASEURL
              value: ""
            - name: ND_ENABLESHARING
              value: "true"
            - name: ND_ENABLEUSEREDIT
              value: "true"
          ports:
            - containerPort: 4533
          volumeMounts:
            - mountPath: /data
              name: data-volume
            - mountPath: /music
              name: music-volume
      volumes:
        - name: data-volume
          persistentVolumeClaim:
            claimName: navidrome-data-pvc
        - name: music-volume
          persistentVolumeClaim:
            claimName: navidrome-music-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: navidrome
  namespace: navidrome
spec:
  selector:
    app: navidrome
  ports:
    - protocol: TCP
      port: 4533
      targetPort: 4533
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: navidrome-ingress
  namespace: navidrome
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-dns
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    nginx.ingress.kubernetes.io/hsts: "true"
    nginx.ingress.kubernetes.io/hsts-max-age: "31536000"
    nginx.ingress.kubernetes.io/hsts-include-subdomains: "true"
    nginx.ingress.kubernetes.io/hsts-preload: "true"
    nginx.ingress.kubernetes.io/limit-connections: "20"
    nginx.ingress.kubernetes.io/limit-rps: "10"
    nginx.ingress.kubernetes.io/limit-burst-multiplier: "5"
    nginx.ingress.kubernetes.io/x-frame-options: "DENY"
    nginx.ingress.kubernetes.io/x-content-type-options: "nosniff"
    nginx.ingress.kubernetes.io/referrer-policy: "strict-origin-when-cross-origin"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - navidrome.example.com
      secretName: navidrome-tls
  rules:
    - host: navidrome.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: navidrome
                port:
                  number: 4533

Manifest Explanation

  • Navidrome will be installed to namespace: navidrome
  • Two PersistentVolumeClaim will be created to store Navidrome data (200Mi) and music files (1Gi).
  • The latest image is image: deluan/navidrome:0.58.0 at the time of writing this.
  • The env values are configured as per Navidrome Configuration Options.
  • Take note of the mountPath: /music for Syncthing later on.
  • Nginx ingress plus Let’s Encrypt to domain host: that maps to Navidrome service at TCP port: 4533

K3s Rancher Dashboard: Pods, PVC, Services

Pods: Navidrome 1/1, Syncthing 1/1
PersistentVolumeClaims: Navidrome 200MB (data) + 500MB (music), Syncthing 100MB (data)
Services: Navidrome 4533/TCP (Web GUI), Syncthing 8384/TCP (Web GUI) and 22000/TCP (Sync protocol)

Step 2 – Syncthing Kubernetes Manifest

Syncthing is a peer-to-peer file synchronization tool that is open-source and does not require cloud services. It enables users to directly sync files between devices. Key features include peer-to-peer sync, cross-platform, open source & privacy-focused, versioning, encrypted communication, and even the incorporation of a web GUI and API. Its core purpose is to

  • Keep files synchronized across multiple devices automatically.
  • Works directly between your devices using your network or the internet.
  • No central server is required, though you can run a dedicated node if desired.

Because Navidrome is exclusively a music streaming server and does not offer file upload functions or storage management, we require Syncthing to transfer our local music files to the remote Navidrome /music folder. Standard ways to copy music files directly into Navidrome’s music folders include SFTP/SCP, SMB/NFS share, and more. But since we’re using PVC (Persistent Volume Claim) and Kubernetes pods, an app like Syncthing will simplify our lives by automatically mirroring our local music folder to Navidrome’s music folder.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: syncthing
  namespace: navidrome
spec:
  replicas: 1
  selector:
    matchLabels:
      app: syncthing
  template:
    metadata:
      labels:
        app: syncthing
    spec:
      containers:
      - name: syncthing
        image: syncthing/syncthing:latest
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8384 # Web UI
        - containerPort: 22000 # Sync
        # env section not required, because
        # 1. The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.
        # 2. Can set GUI Authentication User and Authentication Password
        #env:
        #- name: STGUIADDRESS
        #  value: "0.0.0.0:8384"
        #- name: STGUIAUTH
        #  valueFrom:
        #    secretKeyRef:
        #      name: syncthing-gui-secret
        #      key: STGUIAUTH
        volumeMounts:
        - name: syncthing-config
          mountPath: /config
        - name: music
          mountPath: /music
        resources:
          requests:
            cpu: 200m
            memory: 256Mi
          limits:
            cpu: 500m
            memory: 512Mi
      volumes:
      - name: syncthing-config
        persistentVolumeClaim:
          claimName: syncthing-config-pvc
      - name: music
        persistentVolumeClaim:
          claimName: navidrome-music-pvc
---
apiVersion: v1
kind: Service
metadata:
  name: syncthing
  namespace: navidrome
spec:
  selector:
    app: syncthing
  ports:
  - name: http
    protocol: TCP
    port: 8384      # UI port
    targetPort: 8384
  - name: sync
    protocol: TCP
    port: 22000     # Sync protocol
    targetPort: 22000
  type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: syncthing-ingress
  namespace: navidrome
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-dns"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "50m"
    # Optional security headers
    nginx.ingress.kubernetes.io/hsts: "true"
    nginx.ingress.kubernetes.io/hsts-max-age: "31536000"
    nginx.ingress.kubernetes.io/hsts-include-subdomains: "true"
    nginx.ingress.kubernetes.io/hsts-preload: "true"
    nginx.ingress.kubernetes.io/x-frame-options: "DENY"
    nginx.ingress.kubernetes.io/x-content-type-options: "nosniff"
    nginx.ingress.kubernetes.io/referrer-policy: "strict-origin-when-cross-origin"
spec:
  ingressClassName: nginx   # <---- here
  rules:
  - host: syncthing.example.com   # <== Change to your actual domain
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: syncthing
            port:
              number: 8384
  tls:
  - hosts:
    - syncthing.example.com
    secretName: syncthing-tls
---
# Syncthing needs a directory to store its own config (GUI settings, device IDs, folder list, keys, etc).
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: syncthing-config-pvc
  namespace: navidrome
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi
  storageClassName: longhorn

The deployment YAML is structurally similar to Naivdrome’s above, except it uses 1 PVC only.

Step 3 – Using Syncthing for Navidrome

  1. Navidrome should be up and running, and an admin user should be created when the Navidrome webUI is first loaded. No playlists or sample songs are available for testing or demonstration purposes.
  2. Make sure that remote Syncthing is up and running. When we open the Syncthing web URL, it will ask us to create a GUI authentication user and password. I suggest that you do this and also check the box that says “Use HTTPS for GUI
  3. Download Syncthing for your local setup; the Syncthing Windows Setup, a feature-rich yet portable Windows installer, is used in this tutorial.
  4. In Windows, run the Start Syncthing app, then click the Syncthing Configuration Page desktop shortcut or open http://127.0.0.1:8384/ in your browser. There is no need to create GUI authentication or tick the Use HTTPS for GUI checkbox because the ‘local’ app can only be accessed within your home network.
  5. At the local Syncthing webUI, click + Add Folder > Enter Folder Label (for example, techsch-music) and Folder Path (for example, C:\Users\Techsch\Music) > Save
  6. At the local Syncthing webUI, click + Add Remote Device > Copy remote Syncthing Device ID by clicking Actions > Show ID > Copy > Paste into local Syncthing Device ID > Sharing tab > Shared Folders > Check folder: techsch-music > Give a Device Name (for example, Navidrome-remote) > Save
  7. Between both local and remote Syncthing web GUIs, in the Folders and Remote Devices section, click Edit > Sharing tab > Check music folder and device respectively > Save
  8. Within a minute or two, the sync job will run automatically, and then refresh in Navidrome webUI to see the “Recently Added” songs.

This process may appear tedious; however, it is necessary to configure it once initially. Subsequently, the Start Syncthing service will automatically transfer your music files to the Navidrome library.

How to increase connection sync transfer speed?

If you see Connection Type: Relay WAN, then the sync transfer speed will be slow. This would mean your local and remote Syncthing could not establish a direct peer-to-peer (P2P) connection, so Syncthing is routing traffic through a public relay server over the internet (Wide Area Network = WAN).

Relay WAN which is the default starting connection type is the slowest.

How to fix “Relay WAN” and get a FAST direct connection?

Get QUIC (WAN) with UDP or TCP (WAN) but avoid Relay (WAN)
  1. Port Forwarding
    – On your NAS or local PC side, forward TCP port 22000 to the machine running Syncthing.
    – Also open UDP port 22000 if possible (helps with QUIC protocol).
    – Make sure your router/firewall allows inbound on these ports.
  2. Log into your router:
    – For Asus, Advanced Settings > NAT Forwarding > Port Forwarding
    – Service Name: syncthing
    – Source IP: Leave blank or 0.0.0.0
    – External Port: 22000
    – Internal Port:  22000
    – Internal IP: Your PC’s local IP (Command Prompt ipconfig) or Device Name
    – Protocol: TCP (and UDP if possible)
  3. Allow Syncthing through Windows Firewall
    – Open Windows Defender Firewall
    – Click Allow an app or feature through Windows Defender Firewall
    – Find Syncthing in the list and make sure it’s allowed on private and public networks
    – If not listed, click Allow another app… and add Syncthing manually
    – Also, allow port 22000 TCP and UDP inbound rules if you want to be extra sure.

The connection should now be set to TCP or QUIC (WAN). Alternatively, you may restart both the local and remote Syncthing app. I only have about 100 music files in my library, and it only takes me 1-2 seconds for a 10 MB *.mp3 file to sync with my TCP (WAN) connection.

Troubleshooting

The only major issue I encountered was that Syncthing was unable to write music files into Navidrome’s /music PVC due to permission issues.
2025-10-21 23:38:12: Failed to create path for auto-accepted folder syn223j-music-syncthing at path syn223j-music-syncthing: mkdir /syn223j-music-syncthing: permission denied
ChatGPT recommends adding securityContext, runAsUser and runAsGroup and kubectl exec -it pod/syncthing-7c6fbb7c47-gm8wx -n navidrome -- chown -R 1000:1000 /music && chmod -R 755 /music but none resolved the problem. What I did was to delete all the folders and devices back to the initial state in Syncthing WebUI, and I went to reload the Navidrome pods in my K3s. Then I repeated Step 3 – Using Syncthing for Navidrome—after which everything fell into place.

Syncthing – Connection Error

Step 4 – Navidrome Clients

Navidrome implements a substantial portion of the Subsonic API. There are a few, but not many, Subsonic clients in the app market, depending on whether your phone is Android or iOS. For the iOS App Store, there are SubStreamer and Amperfy Music. I only manage to find Symfonium app available in the Google Play Store; after the trial period ends, it becomes a paid app, but many users say it’s worth the money.

Conclusion

Honestly, I don’t use Navidrome every day; instead, I go to YouTube and choose songs based on how I’m feeling. If you are abroad, you will be wasting valuable paid roaming data because Navidrome needs a data connection to stream your music while you are away from your local telco. There are other uses for Syncthing, such as syncing your local backups to the cloud virtually instantly, but I’ve already implemented my own 3-2-1 backup plan, which backs up my data to cloud object storage using the Synology NAS Hyper Backup app. Because it is easier to copy files to an Android device than an iPhone, I will continue to host my Navidrome and Syncthing project because I find it convenient to stream audio files without having to copy them to my iPhone storage!

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *