If you have spent years building a music collection - ripped CDs, Bandcamp purchases, DRM-free downloads - it probably sits in a folder you can only reach from one computer. Meanwhile you pay a monthly fee to stream music you do not own, with songs that quietly disappear when a licensing deal expires. Navidrome fixes both problems. It is a tiny, fast music server that turns your library into a streaming service: a clean web player, your own playlists, play counts, smart playlists, and access from any device through the wide Subsonic app ecosystem.
This guide walks through a production-ready Navidrome install on a VPS using Docker, with Caddy in front for automatic HTTPS. It covers the parts that trip people up: file tagging, transcoding for slow connections, the Subsonic API that unlocks dozens of mobile apps, and scrobbling to Last.fm or ListenBrainz so you keep your listening history.
music.example.com at your serverdocker-compose.ymldata (database) and music (read-only library)Total time: about 15 minutes.
80 and 443 open to the internet (Let's Encrypt needs them)Navidrome is one of the lightest media servers you can run. The whole binary is a few tens of megabytes, the database is SQLite, and streaming a file is cheap. The only heavy operation is on-the-fly transcoding, which is optional. Disk for the music is the real sizing question.
In your DNS provider, create an A record:
music.example.com → YOUR_VPS_IPV4
Add an AAAA record too if your server has IPv6. Confirm it resolves:
dig +short music.example.com
The output should be your VPS IP. Caddy cannot issue a certificate until DNS points at the box.
On a fresh Ubuntu 22.04 or 24.04 server:
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
Check it works:
docker --version
docker compose version
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Caddy needs 80 for the ACME HTTP challenge and 443 for HTTPS. Do not expose Navidrome's internal port 4533 to the internet - Caddy is the only thing that should answer publicly.
sudo mkdir -p /opt/navidrome
cd /opt/navidrome
sudo mkdir -p data music caddy-data caddy-config
The two data directories map to how Navidrome splits state:
data/ holds the SQLite database, the search index, cached artwork, and your settings, playlists, and play countsmusic/ is your library, mounted read-onlyIf your VPS has a large data disk mounted elsewhere, point the music folder at it:
sudo mkdir -p /mnt/storage/music
sudo ln -s /mnt/storage/music /opt/navidrome/music
Navidrome builds its entire library from the tags embedded in your files, not from folder names. Good tags mean clean albums, correct artists, and working browse-by-genre. Bad tags mean a hundred "Unknown Artist" entries. Before you upload anything, run your collection through a tagger like MusicBrainz Picard, which matches files against the MusicBrainz database and writes consistent tags automatically.
The tags that matter most:
Album Artist - this is what Navidrome groups by, not Artist. Set it on every track or compilations will scatter into one entry per featured artist.Album, Title, Track Number, Disc Number - the basics that order an album correctly.Date or Year, Genre - for browsing and smart playlists.A clean folder layout helps you stay organized even though Navidrome ignores it for grouping:
music/
The National/
2019 - I Am Easy to Find/
01 - You Had Your Soul with You.flac
02 - Quiet Light.flac
Khruangbin/
2018 - Con Todo el Mundo/
01 - Cómo Me Quieres.flac
Navidrome plays FLAC, MP3, AAC, Ogg, Opus, WavPack, and more. Keep your originals in whatever quality you ripped them at - transcoding for slow connections happens on demand without touching the source files.
Create /opt/navidrome/docker-compose.yml:
services:
navidrome:
image: deluan/navidrome:latest
container_name: navidrome
restart: unless-stopped
user: "1000:1000"
environment:
ND_SCANSCHEDULE: "1h"
ND_LOGLEVEL: "info"
ND_BASEURL: "https://music.example.com"
ND_ENABLETRANSCODINGCONFIG: "true"
TZ: "Europe/Berlin"
volumes:
- ./data:/data
- ./music:/music:ro
networks:
- musicnet
caddy:
image: caddy:2
container_name: navidrome-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
networks:
- musicnet
networks:
musicnet:
Notes:
user: "1000:1000" line runs Navidrome as your first non-root user. Match it to whoever owns the music files. Run id -u and id -g to confirm.music is mounted :ro (read-only) on purpose. Navidrome never needs to write to your library, and read-only mounts mean a bug or a bad actor cannot delete your collection.ND_SCANSCHEDULE: "1h" re-scans for new files every hour. Set it to "0" to disable scheduled scans and trigger them manually instead.ND_ENABLETRANSCODINGCONFIG: "true" unlocks the transcoding settings in the web UI. Leave it on if you want to stream lower bitrates on mobile data.4533 stays on the musicnet Docker network. Caddy reaches it by container name.Create /opt/navidrome/Caddyfile:
music.example.com {
encode zstd gzip
reverse_proxy navidrome:4533 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
header_up Host {host}
}
}
The X-Forwarded-Proto header is the one that matters most. Without it Navidrome can build http:// links internally and the Subsonic apps refuse to connect over mixed-content rules. Caddy handles the certificate, renewal, and HTTP-to-HTTPS redirect with no extra config.
cd /opt/navidrome
sudo docker compose up -d
sudo docker compose logs -f
Wait until the Caddy logs show the Let's Encrypt certificate was issued, then open https://music.example.com. You should land on the Navidrome account-creation screen.
The first screen asks you to create the admin user. This is the all-powerful account, so pick a strong password - there is no email recovery flow, only resetting it from the command line. Once you are in, Navidrome immediately starts its first scan in the background.
You can watch progress in the logs:
sudo docker compose logs -f navidrome
A few thousand tracks index in a minute or two. Large lossless libraries take longer because Navidrome reads the tags and extracts embedded artwork from every file. Let the first scan finish before you judge whether anything is missing.
Navidrome has proper multi-user support with per-user playlists, favorites, and play counts. Add accounts under Settings -> Users -> Add:
Each user gets their own listening data, so your partner's heavy-rotation playlist never pollutes your own recommendations and play counts.
This is where Navidrome shines. It implements the Subsonic API, an open standard supported by a large family of polished third-party apps. You are not locked into one official client - pick the player you like best:
In any of these apps, choose Subsonic or Navidrome as the server type and enter:
https://music.example.comMost apps support offline downloads, gapless playback, and scrobbling. Pull an album over Wi-Fi before a flight and it plays without a connection.
http:// or a raw IP address. Subsonic apps and modern browsers block insecure media connections, and your password would travel unencrypted on every request. The Caddy setup above gives you a valid certificate for free - always use the https:// hostname.
If you have years of Last.fm history, you do not have to abandon it. Navidrome scrobbles natively to both Last.fm and the open ListenBrainz. Each user links their own account under Settings -> Personal -> Last.fm / ListenBrainz, authorizes once, and every play from the web player or a connected app gets recorded. ListenBrainz is the better long-term choice if you want your data exportable and not tied to a single company.
Streaming a 1411 kbps FLAC over hotel Wi-Fi or capped mobile data is painful. Navidrome can transcode on the fly to a smaller format without altering your originals. Because you set ND_ENABLETRANSCODINGCONFIG: "true" earlier, the controls live under Settings -> Transcoding in the web UI - choose a player profile and a max bitrate (192 kbps Opus or MP3 is a sweet spot for mobile).
Transcoding uses ffmpeg, which ships inside the official Navidrome image, so there is nothing extra to install. It does use CPU per active stream, so on a small shared VPS keep concurrent transcoded streams modest. For listeners at home on a fast line, leave them on the original-quality profile and skip transcoding entirely.
Your music files are presumably backed up already. The piece that is unique to Navidrome - playlists, play counts, favorites, ratings, and user accounts - lives in the data directory. Losing it means rebuilding every playlist by hand. Pair it with a nightly restic job to object storage:
restic -r s3:s3.amazonaws.com/my-backup-bucket backup /opt/navidrome/data
The directory is small - usually well under a gigabyte even for a big library - so backups are fast and cheap. Restoring is just dropping the folder back and starting the container.
If the only people using your server live in your house, you do not need it on the public internet at all. Putting Navidrome behind Tailscale removes every bot scan and login-bruteforce attempt in one move. Skip the firewall openings for 80 and 443, reach music.example.com over your tailnet, and the Subsonic apps connect exactly the same way. A WireGuard VPN on your VPS works just as well if you already run one.
Caddy returns a 502 right after docker compose up. Navidrome takes a few seconds to initialize its database on first boot, and Caddy can hit it first. Wait 30 seconds and reload. If it sticks, check docker compose logs navidrome.
The library is empty after the scan. Almost always a path or permission issue. Confirm your files live under the folder mounted at /music and that the container user can read them: docker exec -it navidrome ls -la /music. If you see permission denied, fix ownership on the host with sudo chown -R 1000:1000 /opt/navidrome/music.
Albums are split into many entries or show "Unknown Artist". This is a tagging problem, not a Navidrome bug. Set the Album Artist tag consistently across every track of an album and re-run a scan. MusicBrainz Picard fixes this in bulk.
A Subsonic app says "wrong username or password" with correct credentials. Some older apps default to a legacy authentication mode. In the app's server settings, try toggling between the modern token auth and legacy plain auth, and make sure the URL has no trailing path - just https://music.example.com.
Transcoding does nothing or the bitrate is wrong. Confirm ND_ENABLETRANSCODINGCONFIG is true and that the player profile in Settings -> Transcoding is assigned to the right client. Some apps request the original file regardless of the server profile, so also check the app's own quality settings.
data folder into a nightly restic job so a dead disk never costs you your playlists and play history.That's it. Self-hosted Navidrome gives you a fast, private streaming service built from music you actually own, reachable from any device, with no subscription, no ads, and no songs vanishing from your library because a licensing deal lapsed.
Need a VPS for your music collection? Our Linux plans ship with NVMe storage, generous bandwidth, and a storage tier for big lossless libraries. See the options.