Deploy Self-Hosted Music Streaming
Tired of subscription fatigue? I built my own music streaming service in my homelab last year and haven't looked back. Whether you want to stream your 50,000-track FLAC collection or build a family music library without monthly fees, self-hosted music streaming gives you complete control, privacy, and flexibility. In this guide, I'll walk you through deploying Navidrome—my current favorite—using Docker Compose, complete with reverse proxy setup and mobile access.
Why Self-Host Your Music?
Music streaming subscriptions cost $11–17 per month. Over five years, that's $660–1,020 gone. Beyond cost, self-hosting offers genuine advantages: you own your metadata, you keep your listening history private, you can organize music exactly how you want, and you're not at the mercy of catalog changes or service shutdowns.
I evaluated three solid options before settling on Navidrome. Jellyfin is excellent if you want a unified media center (music + video + photos). Subsonic has been around forever and has the deepest client ecosystem. Navidrome is lightweight, fast, and has a beautiful modern interface—it's what I run today, and it uses minimal resources on my old NUC.
Prerequisites
You'll need a Linux host with Docker and Docker Compose installed. I'm running this on a small VPS from RackNerd (2 vCPU, 2GB RAM) and it handles everything beautifully. Alternatively, any home server works: Raspberry Pi 4, old laptop, or dedicated homelab box.
You'll also want:
- Your music library accessible locally (or mounted via NFS/SMB)
- A reverse proxy like Caddy or Nginx Proxy Manager (for HTTPS and remote access)
- A domain name or Tailscale tunnel for external streaming
- At least 2GB free disk space for the Navidrome database
Deploy Navidrome with Docker Compose
Navidrome is my choice because it's resource-efficient, supports SubsonicAPI compatibility (which means hundreds of existing clients work), and it has a lovely modern web UI. Here's my production compose file:
version: '3.8'
services:
navidrome:
image: deluan/navidrome:latest
container_name: navidrome
restart: unless-stopped
ports:
- "4533:4533"
environment:
ND_LOGLEVEL: info
ND_MAXOUTPUTLOGSIZE: "10"
ND_BASEURL: "https://music.example.com"
ND_ENABLEGRAVATAR: "false"
ND_ENABLESTARRATING: "true"
ND_MAXPLAYLISTSIZE: "10000"
ND_ENABLESHARING: "true"
ND_COVERARTWITHFILEBROWSING: "true"
volumes:
- ./data:/data
- /mnt/music:/music:ro
networks:
- caddy-net
caddy:
image: caddy:latest
container_name: caddy-music
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy_data:/data
- ./caddy_config:/config
networks:
- caddy-net
depends_on:
- navidrome
networks:
caddy-net:
driver: bridge
Now create the Caddyfile in the same directory:
music.example.com {
reverse_proxy navidrome:4533 {
header_uri /rest/* /rest/*
header_uri /share/* /share/*
}
}
Replace music.example.com with your actual domain. If you're using a VPS, make sure your DNS A record points to it. If you're on a home network, use Caddy with Tailscale DNS or Cloudflare Tunnel.
Spin it up:
docker compose up -d
Wait 10 seconds, then check the logs:
docker logs navidrome
You should see something like: Navidrome server is running at http://navidrome:4533. Visit https://music.example.com in your browser. If Caddy hasn't obtained an HTTPS certificate yet (wait 30 seconds), refresh. You should see the Navidrome login screen.
docker logs -f navidrome to watch the progress.Configure Music Library Scanning
By default, Navidrome watches your music folder for changes. I keep mine mounted at /mnt/music and it's read-only from the container, which is safer. After the initial scan, Navidrome detects new files every 60 seconds.
To organize your music, Navidrome respects standard ID3 tags. It scans for these folder structures:
Music/Artist/Album/Track.mp3(preferred)Music/Artist - Album/Track.mp3- Flat folder with properly tagged files
I use a quick script to verify my tags before uploading:
#!/bin/bash
# Check for missing ID3 tags
for file in /mnt/music/**/*.mp3; do
artist=$(ffprobe -v error -select_streams a:0 -show_entries format_tags=artist -of default=noprint_wrappers=1:nokey=1:noescape=1 "$file")
title=$(ffprobe -v error -select_streams a:0 -show_entries format_tags=title -of default=noprint_wrappers=1:nokey=1:noescape=1 "$file")
if [ -z "$artist" ] || [ -z "$title" ]; then
echo "Missing tags: $file"
fi
done
Set Up Remote Access with Tailscale
If you're running this on a home server without a public IP, Tailscale is the easiest way to access your music remotely. Install Tailscale on your server, then create a funnel:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
sudo tailscale funnel on
Tailscale will assign you a public HTTPS URL like https://music-abc123.ts.net. Update your Navidrome environment variable:
ND_BASEURL: "https://music-abc123.ts.net"
Restart the container, and you're accessible from anywhere without opening firewall ports.
sudo ufw allow 443/tcp and run Fail2Ban behind your Caddy reverse proxy for brute-force protection.Mobile Apps and Clients
Because Navidrome speaks the Subsonic API, you have hundreds of client options. My go-to setup:
- iOS: Substreamer or Subsonic (official app) – both handle offline caching
- Android: Symfonium or Ultrasonic – modern, responsive, great UI
- Web: The built-in Navidrome web player (works beautifully on mobile Safari)
- Desktop: Sonixd (Windows/Mac/Linux) – gorgeous Spotify-like interface
In each app, point to your Navidrome server URL (e.g., https://music.example.com or your Tailscale funnel), enter your username/password, and you're streaming.
Backup and Maintenance
Navidrome stores its database and metadata in the ./data volume. Back this up regularly:
docker exec navidrome tar czf /data/backup.tar.gz /data/navidrome.db
docker cp navidrome:/data/backup.tar.gz ./backups/
I schedule this weekly with cron:
0 2 * * 0 cd /opt/navidrome && docker compose exec -T navidrome tar czf /data/backup-$(date +\%Y\%m\%d).tar.gz /data/navidrome.db
Updates are painless—just pull the latest image and recreate the container:
docker compose pull
docker compose up -d --force-recreate
Performance Tuning
If you have a massive library (100,000+ tracks), increase memory allocation and tune the database:
navidrome:
image: deluan/navidrome:latest
mem_limit: 1024m
memswap_limit: 2048m
environment:
ND_DBPATH: /data/navidrome.db
ND_SCANSCHEDULE: "@every 2h"
ND_TRANSCODINGCACHESIZE: "200"
The SCANSCHEDULE parameter controls how often Navidrome looks for new files. I set mine to every 2 hours to balance freshness and CPU usage.
For transcoding (converting FLAC/WAV to MP3 on the fly for bandwidth), Navidrome handles it automatically. If transcoding is slow, bump the container's CPU limits or reduce concurrent transcodes.
Next Steps
Your music streaming service is now live. From here, I recommend:
- Configure Fail2Ban to protect the authentication endpoint against brute-force attacks
- Set up monitoring with Uptime Kuma to alert if the service goes down
- Explore Navidrome's sharing feature—create a guest account for friends to access specific playlists
- If you outgrow Navidrome, migrate seamlessly to Jellyfin (which also speaks Subsonic API)
Self-hosted music streaming takes an evening to set up and gives you freedom for years. Once you've experienced streaming *your* entire library, perfectly organized, with zero ads and zero subscription fees, you won't go back.
Discussion