This post builds on top of my last one where I deployed Navidrome on a K3S cluster with Longhorn volumes mounted to store data and music files.
Before we get started, all my code is stored in the following repo.
semmet95
/
navidrome-deployer
A simple chart to deploy Navidrome on a Kubernetes cluster
Navidrome Deployer
Prerequisites
Before deploying navidrome-deployer, ensure the following tools are installed:
- kubectl - Kubernetes command-line tool
- go - Go programming language
- mage - Go-based task runner
- helm - Kubernetes package manager
- helmfile - Helm values file manager
Installation on a cluster
To install the latest release
helmfile apply -f https://github.com/semmet95/navidrome-deployer/releases/latest/download/helmfile.yaml
To install a specific version
helmfile apply -f https://github.com/semmet95/navidrome-deployer/releases/download/<version>/helmfile.yaml
Local Setup and Deployment
To deploy navidrome-deployer locally, execute the following command:
./scripts/local-deployment.sh
In this post I'll share how I deployed Navidrome on a Civo cluster and exposed interfaces to upload and play music.
The last release I made of navidrome-deployer deployed Navidrome on a K3S cluster and exposed Navidrome UI to play music. However, I was still using kubectl commands to copy music files into Navidrome pods. This will be our starting point.
To begin with, we need some way to mount the volume that stores these music files and provide a UI that lets users upload and manage them. This search led me to Filebrowser.
I must say, integrating Filebrowser was a lot easier than I expected. All I had to do was create a Deployment to run the filebrowser/filebrowser:v2.61.0 image and mount the music-volume PVC that was already being used by Navidrome deployment. Filebrowser lets you manage files under the /srv path, mount that music volume on this path and voilà, you now have a UI to upload music to Navidrome.
I did have to make some config changes though. By default, Filebrowser pod logs the admin user password that you can use to log in and access admin-level settings, but letting everyone log in as the admin is probably not a good idea. Luckily, Filebrowser has an option to let users sign up.
Now I could deploy Filebrowser, log in as the admin, change the password, and enable user sign up. But there's a way to slightly decrease the manual setup by using the filebrowser cli. When I run the following command:
filebrowser config set -s --createUserDir
Filebrowser lets users create new accounts and generates their home directory automatically. This comes with a catch, but we'll dive into that when we get to the code changes section.
Creating public endpoints
Now that I had interfaces to play and upload music, ready to be exposed, I needed to figure out a way to expose them safely. This was a problem because networking is not my strongest suit. Thankfully Civo has a tutorial that tackles this exact problem.
Using Traefik LoadBalancer
When creating a Civo cluster, you can select the Traefik v2 (LoadBalancer) addon, and it will provision a load balancer that will handle all the public traffic. The part that surprised me, pleasantly, was how easy it was to get a self-signed certificate. Just combine cert-manager and Let's Encrypt to issue a Certificate and then use it in Traefik's Ingressroute for incoming HTTPS traffic. I also learned about some "best practices" type middlewares that you can add to the Ingressroute to make it more secure, we'll go over them down the line (as you scroll).
Alright, before we jump into the code, time to see Filebrowser and Navidrome in action.
This is the Filebrowser endpoint which is the uploader interface. In my head, it's like the interface artists could interact with to upload their music (not that it's even remotely close, but I like the idea). You can create a new account, log in and start uploading .mp3 files. It might take a few seconds, but the song will appear on Navidrome UI, ready for you to play and enjoy.
This is the Navidrome interface where you can play the uploaded music. I was hoping to create an "open Spotify web" type experience where you can play music without having to create an account but since I couldn't achieve that, I created a guest account with the following credentials.
username: guest
password: guest
Log in using the guest account, and you can play music uploaded by all the users through the Filebrowser interface.
P.S.: Don't worry, I have configured Navidrome so this password cannot be changed.
Putting it all together, this is what it looks like.
Into the code
And now, we finally dive into the code changes. There's a lot to cover, so I'll group them into sections.
Integrating Filebrowser
When you use Filebrowser UI to upload files, all of them are stored in the /srv directory (or subdirectories under it). This is where our music files will be uploaded, meaning this where Navidrome needs to fetch music from. Navidrome deployment already uses a Longhorn volume to store music files. If we set the access mode of this volume's PVC, music-volume, to ReadWriteMany and mount it in the Filebrowser deployment on the /srv path, we are good to go. All the music files will be shared by both the deployments, Filebrowser manages them, Navidrome plays them.
Now that we have figured out what Filebrowser's integration point is going to be, time to configure it based on our requirements. We need to run the following commands to allow user signups, create user directories, and lock the admin account password.
filebrowser config set -s --createUserDir
filebrowser users update admin --lockPassword
Now here comes the tricky part, you cannot configure Filebrowser database while it's being used, which is not surprising but on top of that, you need to run these commands only after Filebrowser has booted up once and created the admin account and the database. It was at this moment I remembered how useful Helm hooks are.
So following is the strategy I ended up with:
- Store Filebrowser database in a Longhorn volume and mount it in the Filebrowser deployment.
- Create a job,
filebrowser-reconfig, withpost-install,post-upgradehelm hooks that also mounts this volume. This way the job will only run after the Filebrowser deployment is ready, and the database is created. - Run the following commands in the job container to safely update Filebrowser database.
DEPLOYMENT="filebrowser"
# ensure filebrowser deployment is ready
kubectl wait --for=condition=Available deployment/$DEPLOYMENT -n $NAMESPACE --timeout=300s
# store the original replica count
ORIGINAL_REPLICAS=$(kubectl get deployment $DEPLOYMENT -n $NAMESPACE -o jsonpath='{.spec.replicas}')
# scale the deployment down to 0 replicas
kubectl scale deployment $DEPLOYMENT -n $NAMESPACE --replicas=0
kubectl wait --for=delete pod -l app=filebrowser -n $NAMESPACE --timeout=120s
# run the filebrowser cli commands
cd database
filebrowser config set -s --createUserDir
filebrowser users update admin --lockPassword
# scale the deployment back up to the original replica count
kubectl scale deployment $DEPLOYMENT -n $NAMESPACE --replicas=$ORIGINAL_REPLICAS
One last thing, this job runs filebrowser and kubectl CLI commands, so I had to create an image that contains them both. Here is the Dockerfile and the image is built and published every time a new release is created.
There was one more issue I kept running into. Filebrowser pods would often crash with issues related to accessing the database, so as a quick fix I add an init-container to run the following command.
chmod -R 777 /database
And I'm running this init-container and the filebrowser container as root. It's far from ideal but it did fix the issue.
Exposing Filebrowser and Navidrome deployments to public traffic
Earlier I mentioned I was following a Civo tutorial to issue certificates using Let's Encrypt which in turn were supposed to be used by Ingress. However, I wanted to add some security to the public endpoints. After doing a little research I found that if I use Traefik's IngressRoute, I could add Middleware layers to the routes that harden the endpoints against common browser-based attacks. To be specific, I added rate limiting and HSTS enforcement. I doubt that's enough but I think it's a good starting point.
Coming to the IngressRoute itself, I have created 2 routes, one for the Filebrowser deployment and the other for Navidrome. Here I ran into a little challenge. The host name for each route is dynamic because the base domain includes Traefik LoadBalancer ID. Luckily, this ID is stored as an annotation added to the traefik service in kube-system namespace. I'm using the lookup function in _helpers.tpl to fetch this service and in turn, the loadbalancer ID from the annotations.
{{- define "baseDomain" -}}
{{- $svc := (lookup "v1" "Service" "kube-system" "traefik") -}}
{{- $annotations := $svc.metadata.annotations -}}
{{- $loadbalancerId := index $annotations "kubernetes.civo.com/loadbalancer-id" -}}
{{- printf "%s.lb.civo.com" $loadbalancerId -}}
{{- end -}}
Then I'm referring to the baseDomain in my Ingressroute. I also found out that you can specify path patterns in the routes to accept, deny, or redirect traffic. I'm currently using it to deny requests targeting API endpoints and admin level settings. So everything combined, this is how the routes are defined.
routes:
- match: Host(`navidrome.{{ include "baseDomain" . }}`) && ! PathPrefix(`/api/user`)
- match: Host(`navidrome-uploader.{{ include "baseDomain" . }}`) && ! PathPrefix(`/api/settings`) && ! PathPrefix(`/settings/global`) && ! PathPrefix(`/settings/users`)
Minor test updates
To ensure I have basic smoke tests to cover these changes, I have updated them to verify cert-manager deployments are healthy and that the Filebrowser DB configuration job is completed successfully.
In my last article I mentioned that I was disabling firewall service to create a K3S cluster and install Navidrome Deployer chart locally. This was not ideal and after reading K3S docs I found out that I could just add some exceptions to the firewalld service to whitelist K3S cluster and allow inter-pod communication by running the following commands in my test setup script.
firewall-cmd --permanent --add-port=6443/tcp
firewall-cmd --permanent --zone=trusted --add-source=10.42.0.0/16
firewall-cmd --permanent --zone=trusted --add-source=10.43.0.0/16
firewall-cmd --reload
Having covered everything, this is the final version of my helmfile.
helmDefaults:
timeout: 600
wait: true
waitForJobs: true
repositories:
- name: longhorn
url: https://charts.longhorn.io
- name: cert-manager
url: quay.io/jetstack/charts
oci: true
- name: navidrome
url: ghcr.io/semmet95/navidrome-deployer/charts
oci: true
releases:
- name: cert-manager
namespace: cert-manager
chart: cert-manager/cert-manager
version: v1.19.4
createNamespace: true
values:
- crds:
enabled: true
- cainjector:
resources:
requests:
cpu: 50m
limits:
memory: 256Mi
- name: longhorn
namespace: longhorn-system
chart: longhorn/longhorn
version: 1.11.0
createNamespace: true
values:
- longhornUI:
replicas: 0
- name: navidrome
namespace: navidrome-system
chart: navidrome/navidrome-deployer
version: 0.21.2
createNamespace: true
disableValidationOnInstall: true
needs:
- cert-manager/cert-manager
- longhorn-system/longhorn
If you have a cluster with Traefik installed, you can install Navidrome Deployer with this one-liner.
helmfile apply -f https://github.com/semmet95/navidrome-deployer/releases/download/v0.21.2/helmfile.yaml
Even though it's just meant to be a demo instance, I know there's a lot of room for improvement, so that's what I would end this article with.
- Currently accessing Navidrome requires sharing credentials. I need to find a way to either let users access the music without requiring an account, or to automate that process behind the scenes in a way that scales up well.
- Filebrowser and Navidrome containers are running as root. They are sharing a PVC so I need to switch to a non-root user without having them interfere with each other's access to the shared files.
- I have also observed significant delay (up to 30 seconds) on Navidrome's side before it imports newly uploaded music. Some discussions around this issue suggest that it could be because of auto generated directories like
lost+foundthat Navidrome watcher might not have access to, leading to it crashing. I've added apostStarthook to Navidrome and Filebrowser containers to delete this directory but that doesn't seem to have any effect. - I would also like to add some restrictions to Filebrowser so users may only upload MP3/audio files.
Hopefully the next time I write about this project all of these would be resolved. If you, the reader, have any suggestions for me feel free to drop them in the comment section. Until next time 🫡

Top comments (0)