Navidrome is one of the easiest self-hosted media wins.

It is not trying to become a whole media-center suite. It has one job: serve your music library cleanly from your own machine, through a web UI and the Subsonic/OpenSubsonic ecosystem.

That focus is why the Docker setup is so tidy: one container, one data folder, one read-only music mount.

What is Navidrome?

Navidrome is a GPL-3.0 web-based music collection server and streamer.

I analyzed commit:

16567f147b6a57f9cdfc4fe37c4577cf0a4b1d57

The latest release observed during this pass was:

v0.64.0 - published 2026-09-12

Navidrome handles large music collections, reads existing metadata, supports compilations and multi-disc albums, offers per-user play counts and favorites, monitors libraries for changes, supports lyrics, has a responsive Material UI based web interface, works with Subsonic-compatible clients, and can transcode on the fly.

Why Self-Host It?

If you already have a curated music folder, Navidrome is a strong fit.

You get:

  • browser playback;
  • mobile access through Subsonic/OpenSubsonic clients;
  • multi-user listening state;
  • playlists and favorites;
  • library scanning and metadata indexing;
  • lyrics;
  • public sharing;
  • FFmpeg-based transcoding;
  • low operational overhead;
  • no external database service.

The last point matters. Navidrome uses SQLite in its /data folder, so a small homelab deployment does not need Postgres, MariaDB, Redis, S3, or a job queue.

Tech Overview

The backend is Go. The server uses Cobra for commands, Chi for HTTP routing, SQLite with Goose migrations, repository layers over the database, FFmpeg/mpv runtime tooling, Prometheus support, and optional integrations for Last.fm, ListenBrainz, Deezer, Jellyfin-style clients, and plugins.

The web UI lives in ui/ and uses:

  • React 17;
  • React Admin;
  • Material UI 4;
  • Redux and Redux Saga;
  • Vite 7;
  • vite-plugin-pwa;
  • Workbox.

The server mounts:

/api    Native API
/rest   Subsonic/OpenSubsonic API
/share  Public share endpoints
/app    Web UI
/ping   Healthcheck

The scanner is a multi-phase pipeline. It walks your music folder, reads metadata, updates folders and tracks, detects missing or moved files, refreshes album state, imports playlists, updates artwork queues, and stores everything in SQLite.

The UI Is a PWA

Navidrome’s web UI is installable.

The Vite config uses VitePWA with injectManifest, a custom src/sw.js, Workbox, app icons, standalone display mode, and a manifest named manifest.webmanifest.

There is one important implementation detail: index.html is intentionally not precached, because Navidrome renders user-specific configuration and auth-related data into it.

So yes, it is a PWA. But no, it is not an offline music library. The app shell and offline fallback can be cached, while browsing, search, streaming, scrobbling, and state changes still require the Navidrome server.

Self-Hosting with Docker

The official Docker docs use the deluan/navidrome image, /data for the database/cache, and /music as a read-only mount.

They also call out one common trap: PUID and PGID are not how this image works. Use Compose’s user: directive with a real UID:GID that can write the data folder and read the music folder.

The reusable Home-Lab compose file is here:

The site can include the same snippet from the Home-Lab submodule:

assets/snippets/navidrome/docker-compose.yml
#version: '3'

services:
  navidrome:
    image: deluan/navidrome:latest
    container_name: navidrome
    user: "1000:1000" # You can adjust this to your user's UID and GID for permissions
    ports:
      - "4533:4533"
    environment:
      # Optional: set timezone to your local one. Example: "Europe/Warsaw"
      - TZ=Europe/Warsaw
      - ND_SCANINTERVAL=1h # Scan for new music every hour
    volumes:
      # This is the directory where Navidrome will store its database and settings
      - /home/jalcocert/Home-Lab/navidrome/data:/data
      # This is the directory where your music files are located
      - /home/jalcocert/Home-Lab/navidrome/music:/music:ro
    restart: unless-stopped

Create the local config:

cd assets/snippets/navidrome
cp .env.sample .env
mkdir -p data backups

Edit:

NAVIDROME_MUSIC_PATH=/path/to/your/music
NAVIDROME_UID=1000
NAVIDROME_GID=1000

Then make sure the selected user can write data/backups and read music:

sudo chown -R 1000:1000 data backups
chmod -R u+rwX,go-rwx data backups
chmod -R u+rX,go-rwx /path/to/your/music

Start it:

docker compose up -d

Open:

http://localhost:4533

Navidrome redirects to:

http://localhost:4533/app/

On first login, create the admin account.

Configuration Notes

Navidrome supports environment variables, command-line flags, and config files. The precedence is:

environment variables > command-line arguments > config file

Useful Docker variables:

  • ND_LOGLEVEL=info
  • ND_BASEURL=
  • ND_ENABLEINSIGHTSCOLLECTOR=false
  • ND_SCANNER_SCHEDULE=@every 1h
  • ND_SESSIONTIMEOUT=48h
  • ND_BACKUP_PATH=/backups
  • ND_BACKUP_SCHEDULE=@every 24h
  • ND_BACKUP_COUNT=7

The current scanner schedule option is Scanner.Schedule, which maps to:

ND_SCANNER_SCHEDULE

You may still find older snippets using ND_SCANINTERVAL or ND_SCANSCHEDULE. For a new deployment, use ND_SCANNER_SCHEDULE.

Reverse Proxy Notes

For LAN-only use, http://localhost:4533 is enough.

For remote access, put Navidrome behind HTTPS with Caddy, Traefik, Nginx Proxy Manager, Cloudflare Tunnel, or another proxy you already operate.

If you serve it under a subpath, configure:

ND_BASEURL=/music

If you serve it on a normal subdomain, leave ND_BASEURL empty unless you need a full external base URL for a specific integration.

Field Test

I tested the official Docker image:

docker run -d --name navidrome-foss-trial \
  --user $(id -u):$(id -g) \
  -e ND_LOGLEVEL=info \
  -e ND_ENABLEINSIGHTSCOLLECTOR=false \
  -p 127.0.0.1:14533:4533 \
  -v /tmp/navidrome-foss-data:/data \
  -v /tmp/navidrome-foss-music:/music:ro \
  deluan/navidrome:latest

Results:

  • /ping returned ..
  • / returned HTTP 302.
  • /app/ returned HTTP 200.
  • The page title was Navidrome.
  • navidrome --version reported 0.64.0 (1072e9f7).
  • Logs showed the SQLite schema being created, caches initialized, FFmpeg found at /usr/bin/ffmpeg, and routes mounted for /api, /rest, /share, and /app.

The pinned deluan/navidrome:0.64.0 image has the same image ID as the pulled latest image on this machine, and the manifest includes Linux builds for amd64, arm64, arm/v6, arm/v7, 386, and riscv64.

I validated the reusable Compose file with:

NAVIDROME_MUSIC_PATH=/tmp/navidrome-foss-music \
docker compose -f assets/snippets/navidrome/docker-compose.yml config

FAQ

Does Navidrome need Postgres?

No. The standard deployment uses SQLite in /data.

Can I keep my music read-only?

Yes. The official Docker layout mounts /music read-only. Navidrome stores its database, cache, generated secrets, plugins, and backups in writable paths under /data and /backups.

Does it support mobile apps?

Yes. Navidrome is compatible with Subsonic/OpenSubsonic clients, so you can use the built-in web UI or a third-party mobile/desktop client.

Does it use FFmpeg?

Yes. The official image includes FFmpeg and the smoke test logs confirmed Navidrome found /usr/bin/ffmpeg.

Is it a PWA?

Yes. The web UI uses Vite, vite-plugin-pwa, Workbox, a web manifest, and a service worker. It can be installed like a web app, but it still needs the server online for actual streaming and library operations.

Can it run on a Raspberry Pi?

Yes. The official Docker image is published for ARM platforms, including arm/v6, arm/v7, and arm64.

Is it a Spotify replacement?

It replaces the streaming experience for music you own. It does not provide a commercial music catalog.

Conclusion

Navidrome is a clean self-hosting recommendation because the architecture matches the job.

One container. One SQLite data folder. One read-only music mount. A good web UI. A broad client ecosystem through Subsonic/OpenSubsonic compatibility. FFmpeg when you need transcoding.

For a homelab music library, that is a practical shape.