Music today has almost completely moved into streaming. Services like Spotify, Apple Music or Deezer are convenient – but they come at a price: you pay twice.

On the one hand with money, on the other hand with your own data and dependency on the provider. If the service raises prices tomorrow or a favorite album disappears, you are powerless.

The best solution is as before: own and host your music yourself. With open formats (MP3, FLAC, OGG, …) and a tool like Navidrome you can build your very own personal streaming service.

Setup

The service requires two directories: One for the application and one for the music files:

sudo mkdir -p /opt/music

sudo mkdir -p /opt/navidrome

The music files will later be copied into the directory /opt/music.

As with the other services, Navidrome should again get its own random port:

echo $(shuf -i 1024-65535 -n 1)

In the application directory create the file docker-compose.yaml:

services:
  navidrome:
    image: localhost:5000/deluan/navidrome:latest
    container_name: navidrome
    restart: unless-stopped
    ports:
      - "4533:4533"
    environment:
      - ND_SCANINTERVAL=1m
      - ND_LOGLEVEL=info
      - ND_BASEURL=https://<CLOUD-DOMAIN>:<RANDOM-PORT>
    volumes:
      - /opt/music:/music:ro
      - ./data:/data

Before the container can be started, the image must be mirrored from the official container hub:

docker pull deluan/navidrome:latest

docker tag deluan/navidrome:latest localhost:5000/deluan/navidrome:latest

docker push localhost:5000/deluan/navidrome:latest

Now the service can be started:

docker compose up -d

For external communication NGINX needs an additional configuration called navidrome under /etc/nginx/sites-available:

server {
    listen <RANDOM-PORT> ssl;
    server_name <CLOUD-DOMAIN>;

    ssl_certificate /etc/letsencrypt/live/<CLOUD-DOMAIN>/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<CLOUD-DOMAIN>/privkey.pem;

    client_max_body_size 256M;

    location / {
        proxy_pass http://localhost:4533;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Enable and restart:

sudo ln -s /etc/nginx/sites-available/navidrome /etc/nginx/sites-enabled/

sudo systemctl restart nginx

After opening the firewall on the port:

sudo ufw allow <RANDOM-PORT>

sudo ufw reload

the web interface should be reachable at https://<CLOUD-DOMAIN>:<RANDOM-PORT>

Apps

A list of compatible apps for all common operating systems 👉 https://www.navidrome.org/docs/overview/#apps

Backups

Navidrome stores important data (users, playlists, settings) in the data directory. This should definitely be backed up regularly.

Simply create a plugin under /opt/backup/plugins/navidrome.sh:

#!/bin/bash
PLUGIN_OUTPUT_DIR="$1"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S.%3N')] $*"
}

NAVIDROME_DIR="/opt/navidrome/data"
BACKUP_FILE="$PLUGIN_OUTPUT_DIR/navidrome_data_$(date '+%Y-%m-%d').tar.gz"

log "[INFO] Starting backup of Navidrome data ..."
tar -czf "$BACKUP_FILE" -C "$NAVIDROME_DIR" .

if [ $? -eq 0 ]; then
    log "[INFO] Backup successful: $BACKUP_FILE"
else
    log "[ERROR] Backup failed!"
fi

And make it executable:

chmod +x /opt/backup/plugins/navidrome.sh

Quick & Dirty

DOMAIN=cloud.example.com
PORT=$(shuf -i 1024-65535 -n 1)

sudo mkdir -p /opt/music /opt/navidrome

cat <<EOF > /opt/navidrome/docker-compose.yaml
services:
  navidrome:
    image: localhost:5000/deluan/navidrome:latest
    container_name: navidrome
    restart: unless-stopped
    ports:
      - "4533:4533"
    environment:
      - ND_BASEURL=https://$DOMAIN:$PORT
    volumes:
      - /opt/music:/music:ro
      - ./data:/data
EOF

docker pull deluan/navidrome:latest
docker tag deluan/navidrome:latest localhost:5000/deluan/navidrome:latest
docker push localhost:5000/deluan/navidrome:latest

docker compose -f /opt/navidrome/docker-compose.yaml up -d

sudo tee /etc/nginx/sites-available/navidrome >/dev/null <<EONGX
server {
    listen $PORT ssl;
    server_name $DOMAIN;
    ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem;
    client_max_body_size 256M;
    location / {
        proxy_pass http://localhost:4533;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
        proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto \$scheme;
    }
}
EONGX

sudo ln -s /etc/nginx/sites-available/navidrome /etc/nginx/sites-enabled/
sudo systemctl restart nginx
sudo ufw allow $PORT && sudo ufw reload