Create Private Music Library: Self-Hosted Music with Navidrome
I got tired of Spotify subscriptions, algorithmic recommendations I didn't ask for, and worrying about licensing restrictions on my own music collection. Three months ago, I set up Navidrome—a lightweight, self-hosted music server—in Docker on a spare Hetzner VPS. Now I stream my entire music library (15,000+ tracks) across my phone, tablet, and browser with zero monthly fees and complete control. Here's exactly how I did it.
Why Self-Host Your Music?
Before I dive into the setup, let me be clear about the appeal. Streaming services are convenient, but they're also rent-forever models. If Spotify raises prices or removes an artist's catalog, you're stuck. With a private music library, you own the relationship to your music files.
I prefer Navidrome because it's lightweight—it runs on about 256MB RAM—supports Subsonic API (so it's compatible with dozens of mobile clients), and has a modern web UI. Unlike Jellyfin (which is fantastic but heavier), Navidrome is purpose-built for music, not a general media server. And unlike Plex, there's zero phone-home behavior or proprietary nonsense.
The typical use case: you have your music files (FLAC, MP3, etc.) stored locally or on a file server. Navidrome indexes them, makes them searchable, and exposes a clean interface to stream from anywhere you have internet access.
Prerequisites
You'll need:
- A VPS or home server running Docker and Docker Compose (I use a RackNerd KVM VPS—affordable and stable)
- At least 10GB of free disk space for music files
- A reverse proxy (I recommend Caddy for simplicity)
- Your music collection in a common format (MP3, FLAC, OGG, etc.)
abcde or MusicBrainz Picard. Many homelabbers use YouTube Music's backup feature or legally source FLAC files from Bandcamp artists.Setting Up Navidrome with Docker Compose
I'll walk you through the exact Docker Compose setup I use. This assumes you have Docker and Docker Compose installed. If not, run:
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
Create a directory for your Navidrome setup:
mkdir -p ~/navidrome/{music,data}
cd ~/navidrome
Now, create a docker-compose.yml file:
version: '3'
services:
navidrome:
image: deluan/navidrome:latest
container_name: navidrome
ports:
- "4533:4533"
environment:
ND_LOGLEVEL: info
ND_BASEURL: ""
ND_PORT: "4533"
ND_MUSICFOLDER: "/music"
ND_AUTOIMPORTPLAYLISTS: "true"
ND_ENABLESTARRATING: "true"
ND_DEFAULTLANGUAGE: "en"
volumes:
- ./music:/music:ro
- ./data:/data
restart: unless-stopped
networks:
- caddy_network
networks:
caddy_network:
external: true
Before spinning this up, you'll want to copy your music files into the music directory:
cp -r /path/to/your/music/* ~/navidrome/music/
docker-compose up -d
Check that it's running:
docker-compose logs -f navidrome
You should see scan activity. On first run, Navidrome will index your entire music collection. With 15,000 tracks, this took about 2 minutes on my VPS.
:ro) unless you're using Navidrome's playlist save feature. This prevents accidental writes to your precious music files.Exposing Navidrome with Caddy
Navidrome listens on port 4533 internally, but you'll want to expose it securely over HTTPS. I prefer Caddy because it handles Let's Encrypt SSL automatically.
If you don't already have Caddy running, here's a minimal setup alongside Navidrome. Create a Caddyfile:
music.yourdomain.com {
reverse_proxy localhost:4533
}
Add Caddy to your Docker Compose:
version: '3'
services:
caddy:
image: caddy:latest
container_name: caddy
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
networks:
- caddy_network
restart: unless-stopped
navidrome:
image: deluan/navidrome:latest
container_name: navidrome
ports:
- "4533:4533"
environment:
ND_LOGLEVEL: info
ND_BASEURL: ""
ND_PORT: "4533"
ND_MUSICFOLDER: "/music"
ND_AUTOIMPORTPLAYLISTS: "true"
ND_ENABLESTARRATING: "true"
volumes:
- ./music:/music:ro
- ./data:/data
restart: unless-stopped
networks:
- caddy_network
networks:
caddy_network:
driver: bridge
volumes:
caddy_data:
caddy_config:
Update your DNS to point music.yourdomain.com to your VPS IP, then:
docker-compose down
docker-compose up -d
docker-compose logs caddy
Within seconds, Caddy will provision an SSL certificate and your music server is live at Open The interface is clean: search, browse by artist/album, create playlists, and see what's playing. The web player works well, but the real power comes from mobile clients. Mobile clients I recommend: All of these support the Subsonic API, so they'll work with your Navidrome instance. Just point them to Since your music library is precious, consider regular backups. I use a simple cron job to sync metadata: For automatic image updates, add Watchtower to your compose file: On a budget VPS (like RackNerd's entry-level KVM), Navidrome performs beautifully. Even with 20,000+ tracks and concurrent users, CPU usage stays below 5%. The real bottleneck is bandwidth—streaming FLAC over a slow connection won't work, so transcode on-the-fly if needed. Set For truly massive libraries (50,000+ tracks), consider a more powerful instance or local storage. A Hetzner Dedicated Server with ZFS storage is overkill but a nice option if you have the budget. Your private music library is now live. From here, you might add: Navidrome is lightweight, stable, and exactly what a music lover needs. No algorithms, no subscription creep, no vendor lock-in. Your music, your rules.https://music.yourdomain.com
Accessing Your Music Library
https://music.yourdomain.com in your browser. You'll see a login screen. On first access, use username admin and password admin (change this immediately).
https://music.yourdomain.com, enter your credentials, and stream.Optional: Add Backup and Auto-Updates
0 2 * * * rsync -av ~/navidrome/data/ backup-server:/backups/navidrome/ >> /var/log/navidrome-backup.log 2>&1 watchtower:
image: containrrr/watchtower
container_name: watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: --interval 86400
restart: unless-stoppedPerformance Notes
ND_TRANSCODINGCACHESIZE to enable this.Next Steps