Navidrome
Deployment rootless
1. Prepare the host
Prerequisites once as root
Install Podman (needs podman 4.3 or newer; Ubuntu 22.04 ships 3.4 and is too old, use 24.04)
apt update && apt install -y podman
Create user (if not existing)
useradd -m -s /bin/bash navidrome passwd navidrome
Enable linger (services keep running after logout and start after reboot)
loginctl enable-linger navidrome
Switch to deployment user
su - navidrome
Use su - with the dash — this creates a proper login shell as navidrome. Without the dash, root environment variables carry over and rootless Podman will not work correctly.
Fix environment for rootless systemd (new users only — skip if already set)
Required once so systemctl --user works in non-login sessions.
cat << 'EOF' >> ~/.bashrc # Fix for systemd user services (rootless Podman/systemctl) export XDG_RUNTIME_DIR=/run/user/$(id -u) export DBUS_SESSION_BUS_ADDRESS=unix:path=$XDG_RUNTIME_DIR/bus EOF source ~/.bashrc
Create the host directories
The containers write to these host paths as their own user, so they need the right owner, whatever format you use. Quadlet does not even create a missing one.
mkdir -p /music # podman unshare chown UID:GID /music # owner unknown: the user the container writes as
2. Choose a format and deploy
The same pod in four forms. Pick one and follow only its steps.
Each tab says which podman it needs. Check yours with podman --version:
Debian 12 ships 4.3, Ubuntu 24.04 4.9, Debian 13 5.4, Fedora 5.8, Arch 6.x.
Kubernetes YAML for podman kube play, started by systemd through a Quadlet .kube unit (podman 4.4+; older podman like Debian 12 uses podman-kube@, see the version check below).
Kubernetes YAML
apiVersion: v1
kind: Pod
metadata:
name: navidrome
labels:
app: navidrome
spec:
restartPolicy: Always
containers:
- name: navidrome
image: docker.io/deluan/navidrome:latest
imagePullPolicy: Always
ports:
- containerPort: 4533
hostPort: 4533
env:
- name: ND_SCANSCHEDULE
value: 1h
- name: ND_LOGLEVEL
value: info
- name: ND_SESSIONTIMEOUT
value: 24h
- name: ND_BASEURL
value: ''
- name: TZ
value: Europe/Vienna
volumeMounts:
- name: vol-0
mountPath: /data
- name: vol-1
mountPath: /music
readOnly: true
volumes:
- name: vol-0
persistentVolumeClaim:
claimName: navidrome-data
- name: vol-1
hostPath:
path: /music
type: DirectoryOrCreate
Save the YAML file
mkdir -p ~/.config/containers/ nano ~/.config/containers/navidrome.yaml
Test the pod (without autostart)
podman kube play ~/.config/containers/navidrome.yaml # Check status: podman pod ps && podman ps # Stop: podman kube down ~/.config/containers/navidrome.yaml
Check your podman version
podman --version
4.4 or newer: continue with step 3 below.
Older, e.g. 4.3 on Debian 12? There is no Quadlet yet: use podman's own podman-kube@ service instead of steps 3–5
systemctl --user enable --now podman-kube@$(systemd-escape ~/.config/containers/navidrome.yaml).service # Status & logs: systemctl --user status podman-kube@$(systemd-escape ~/.config/containers/navidrome.yaml).service journalctl --user -u podman-kube@$(systemd-escape ~/.config/containers/navidrome.yaml).service -f
The day-to-day commands below have a block for this service.
Create Quadlet .kube file
[Unit] Description=navidrome pod After=network-online.target Wants=network-online.target [Kube] Yaml=%h/.config/containers/navidrome.yaml [Service] Restart=always [Install] WantedBy=default.target
%h is a systemd home-directory specifier — do not replace it with ~/.
Save to ~/.config/containers/systemd/:
mkdir -p ~/.config/containers/systemd/ nano ~/.config/containers/systemd/navidrome.kube
Start systemd service
systemctl --user daemon-reload systemctl --user start navidrome.service
Status & Logs
systemctl --user status navidrome.service journalctl --user -u navidrome.service -f podman pod ps podman ps
Enable automatic image updates optional
Requires AutoUpdate=registry in the .kube file. Podman then checks for new images and restarts the pod automatically:
systemctl --user enable --now podman.socket systemctl --user daemon-reload systemctl --user enable --now podman-auto-update.timer # Check status: systemctl --user status podman-auto-update.timer # Trigger manually: podman auto-update
Day-to-day logs, restart, update, remove (click to open)
Logs & shell into a container
podman logs -f navidrome-navidrome podman exec -it navidrome-navidrome /bin/sh
Restart
systemctl --user restart navidrome.service
Update an image by hand
Pull the new version, the restart recreates the containers from it:
podman pull <image>:<tag> systemctl --user restart navidrome.service
Stop & remove
Stop through systemd, otherwise Restart=always brings the pod back. To remove it for good, delete the unit files and reload:
systemctl --user stop navidrome.service rm ~/.config/containers/systemd/navidrome.kube systemctl --user daemon-reload
podman older than 4.4 (podman-kube@ from step 3)
systemctl --user restart podman-kube@$(systemd-escape ~/.config/containers/navidrome.yaml).service # Update an image: pull, then restart as above podman pull <image>:<tag> # Stop and remove from autostart: systemctl --user disable --now podman-kube@$(systemd-escape ~/.config/containers/navidrome.yaml).service
Delete the data — permanent, only after stop & remove
podman volume rm navidrome-data podman unshare rm -rf /music
Files created by containers belong to root in the user namespace — podman unshare is required to delete them.
Native systemd units without the Kubernetes YAML: one .pod plus one .container per container.
Each container becomes its own systemd service (restart, logs and ordering per container).
Needs podman 5.0 or newer — Debian 12 ships 4.3, use the Kube YAML tab there.
Save the unit files
[Unit] Description=navidrome pod [Pod] PodName=navidrome PublishPort=4533:4533
[Unit] Description=navidrome-navidrome container [Container] Image=docker.io/deluan/navidrome:latest Pod=navidrome.pod ContainerName=navidrome-navidrome Pull=always Environment=ND_SCANSCHEDULE=1h Environment=ND_LOGLEVEL=info Environment=ND_SESSIONTIMEOUT=24h Environment=ND_BASEURL= Environment=TZ=Europe/Vienna Volume=navidrome-data:/data:Z Volume=/music:/music:ro [Service] Restart=always [Install] WantedBy=default.target
Save all of them to ~/.config/containers/systemd/:
mkdir -p ~/.config/containers/systemd/ nano ~/.config/containers/systemd/navidrome.pod nano ~/.config/containers/systemd/navidrome-navidrome.container
Start the pod
The pod service pulls in every container service. Autostart at boot comes from the [Install] section of the units, no enable needed.
systemctl --user daemon-reload systemctl --user start navidrome-pod.service
Status & Logs
systemctl --user status navidrome-pod.service journalctl --user -u navidrome-navidrome.service -f podman pod ps && podman ps
Day-to-day logs, restart, update, remove (click to open)
Logs & shell into a container
podman logs -f navidrome-navidrome podman exec -it navidrome-navidrome /bin/sh
Restart
systemctl --user restart navidrome-pod.service
Update an image by hand
Pull the new version, the restart recreates the containers from it:
podman pull <image>:<tag> systemctl --user restart navidrome-pod.service
Stop & remove
Stop through systemd, otherwise Restart=always brings the pod back. To remove it for good, delete the unit files and reload:
systemctl --user stop navidrome-pod.service rm ~/.config/containers/systemd/navidrome.pod rm ~/.config/containers/systemd/navidrome-navidrome.container systemctl --user daemon-reload
Delete the data — permanent, only after stop & remove
podman volume rm navidrome-data podman unshare rm -rf /music
Files created by containers belong to root in the user namespace — podman unshare is required to delete them.
A docker-compose.yml for podman-compose. Handy if you already work with Compose; there are no per-container systemd services.
Install podman-compose as root
Run this as root, e.g. together with the prerequisites before su - navidrome.
apt install -y podman-compose
Save the compose file
services:
navidrome:
image: docker.io/deluan/navidrome:latest
pull_policy: always
restart: always
ports:
- 4533:4533
environment:
- ND_SCANSCHEDULE=1h
- ND_LOGLEVEL=info
- ND_SESSIONTIMEOUT=24h
- ND_BASEURL=
- TZ=Europe/Vienna
volumes:
- navidrome-data:/data:Z
- /music:/music:ro
volumes:
navidrome-data: null
mkdir -p ~/navidrome nano ~/navidrome/docker-compose.yml
Start, status & logs
cd ~/navidrome podman-compose up -d podman-compose ps podman-compose logs -f # Stop: podman-compose down
Start after reboot
podman-restart.service starts every container with restart: always at boot:
systemctl --user enable podman-restart.service
Update images
cd ~/navidrome podman-compose pull podman-compose up -d
Day-to-day logs, restart, update, remove (click to open)
Logs & shell into a container
podman logs -f navidrome_navidrome_1 podman exec -it navidrome_navidrome_1 /bin/sh
Restart
cd ~/navidrome podman-compose restart
Stop & remove
cd ~/navidrome podman-compose down
Delete the data — permanent, only after stop & remove
podman volume rm navidrome_navidrome-data podman unshare rm -rf /music
Files created by containers belong to root in the user namespace — podman unshare is required to delete them.
Plain podman pod create / podman create commands. Good for a quick test or to see what happens under the hood.
Save the script
#!/bin/bash set -e # ── Create named volumes ─────────────────────────────────────────── # Named volumes are created automatically — no UID/GID permission adjustment needed. podman volume exists navidrome-data || podman volume create navidrome-data # ── Create host paths ────────────────────────────────────────────── # The container process needs write access. Existing data may need chown -R. mkdir -p /music # podman unshare chown UID:GID /music # owner unknown: the user the container writes as # ── Create pod ───────────────────────────────────────────────────── podman pod create \ --name navidrome \ -p 4533:4533 # ── Container: navidrome-navidrome ───────────────────────────────────────────── podman create \ --pod navidrome \ --name navidrome-navidrome \ --restart always \ --pull always \ -e ND_SCANSCHEDULE='1h' \ -e ND_LOGLEVEL='info' \ -e ND_SESSIONTIMEOUT='24h' \ -e ND_BASEURL='' \ -e TZ='Europe/Vienna' \ -v navidrome-data:/data:Z \ -v /music:/music:ro \ docker.io/deluan/navidrome:latest # ── Start pod ────────────────────────────────────────────────────── podman pod start navidrome # ── Stop & clean up ─────────────────────────────────────────────── # podman pod stop navidrome # podman pod rm navidrome
nano navidrome.sh
Run it, status & logs
bash navidrome.sh podman pod ps && podman ps podman logs -f <container-name>
Start after reboot
podman-restart.service starts every container with --restart always at boot:
systemctl --user enable podman-restart.service
Remove or rebuild
The script creates the pod once. To apply changes or new images, remove the pod and run the script again (named volumes stay):
podman pod rm -f navidrome bash navidrome.sh
Day-to-day logs, restart, update, remove (click to open)
Logs & shell into a container
podman logs -f navidrome-navidrome podman exec -it navidrome-navidrome /bin/sh
Restart
podman pod stop navidrome podman pod start navidrome
Stop & remove
podman pod rm -f navidrome
Delete the data — permanent, only after stop & remove
podman volume rm navidrome-data podman unshare rm -rf /music
Files created by containers belong to root in the user namespace — podman unshare is required to delete them.
Ports < 1024 (e.g. 80, 443)
Rootless cannot open privileged ports. Solution:
sysctl net.ipv4.ip_unprivileged_port_start=80
Make persistent in /etc/sysctl.d/99-podman.conf.
Containers communicate via localhost
All containers in the pod share the same network namespace. Always use localhost, not container names.
# Correct (e.g. app → db): localhost:5432 # Wrong (doesn't work in a pod): db-container:5432
List open ports
Which ports are the running containers publishing? See the PORTS column:
podman ps
Custom DNS for the pod
Set a custom DNS server (e.g. local Pi-hole) in the generator under DNS server. Every format picks it up.
Set volume ownership
Fix permission errors by adjusting UID/GID in the user namespace:
podman unshare chown 1000:1000 /path/to/volume
SELinux volume labels
On SELinux systems (RHEL, Fedora) set the volume suffix:
/host/path:/container/path:Z # private /host/path:/container/path:z # shared
List all volumes
podman volume ls podman volume inspect <volume-name>
Volume backup
Back up data from a named volume:
podman run --rm \ -v <volume-name>:/data:ro \ -v $(pwd):/backup \ busybox tar czf /backup/backup.tar.gz /data
Cleanup
Remove unused images, containers and volumes:
podman system prune -f # containers + images podman image prune -f # untagged images only podman volume prune -f # unused volumes
Check for outdated images
List local images; auto-update only covers units with AutoUpdate= (Kube and Quadlet tabs):
podman images podman auto-update --dry-run
All containers, including stopped ones
podman pod ps podman ps -a
Resource usage
podman stats --no-stream