Acervo musical self-hosted com Navidrome

Introdução
Faz tempo que eu queria tirar minhas músicas de pastas espalhadas do meu notebook e ter algo parecido com um Spotify, só que meu. E eu finalmente resolvi tirar isso do papel.
A motivação é que, basicamente, os arquivos são meus, a maior parte vinda de download codes que acompanham alguma mídia física que comprei, e também o meu próprio catálogo do Bandcamp que muitas vezes não se encontra em nenhum streaming por aí, a qualidade sempre que possível é em FLAC (lossless) e nenhum disco ‘some’ da biblioteca por problemas entre a gravadora dos artistas e o serviço de streaming (cof, cof, Spotify).
Assim consigo ter tudo em um só lugar, disponível offline em casa (pelo endereço local) e também pela internet pelo meu celular ou computador em qualquer lugar do mundo.
Em troca, tem um pouco de esforço para a configuração, o que também chega a ser divertido, e é isso que eu conto aqui em passo a passo.
Por que o Navidrome?
Bom, o Navidrome é uma solução opensource que só lida com música. E só isso foi suficiente para mim. É o foco principal dele, e está sempre bem atualizado (aqui é possível acompanhar os releases: https://github.com/navidrome/navidrome/releases/). É um binário único em Go que roda bem tranquilo containerizado em uma VM pequena e lê as tags dos arquivos.
Também contou a favor ter uma interface web bonitinha e com cores personalizáveis e scrobble nativo pro Last.fm.
A infraestrutura
Em casa eu tenho um Proxmox rodando num nó chamado hyrule. Dentro dele, fiz uma VM com Debian 13. E dentro dela, fiz o setup do Navidrome utilizando Docker - escolhi rodar em Docker devido à facilidade e também porque a mesma VM será utilizada em projectos self-hosted futuramente.

Deixei o /home em um volume separado por ser onde mora o acervo e ele pode (e deve) crescer sem lotar o disco do sistema.
Subindo o Navidrome
O Navidrome precisa de bem pouca coisa. Uma pasta gravável pro banco e o cache, que dentro do container é o /data, e a pasta de música em /music, que pode ser só leitura.
Coloquei também um SCANSCHEDULE de 1h, mas também é possível fazer isso de imediato pela webUI depois de logar.
O docker-compose.yml ficou assim:
services:
navidrome:
image: deluan/navidrome:latest
user: 1000:1000
ports:
- "4533:4533"
restart: unless-stopped
environment:
ND_LOGLEVEL: info
ND_SCANSCHEDULE: 1h
volumes:
- "/opt/navidrome/data:/data"
- "/home/giovanna/music:/music:ro"
(a pasta de dados tem que pertencer ao mesmo UID que está no user: do container, senão ele não consegue gravar o banco)
sudo mkdir -p /opt/navidrome/data
sudo chown 1000:1000 /opt/navidrome/data
cd /opt/navidrome && sudo docker compose up -d


Com o container no ar, é só abrir http://media.home:4533 e criar as credenciais de admin. Depois disso, é por esta tela que se entra:

Organizando o acervo
Vale saber que o Navidrome monta a biblioteca pelas tags dos arquivos, e não pelos nomes das pastas. Então a arrumação começou pelas tags, o que demanda um pouco de trabalho manual se você, assim como eu, preza por uma biblioteca bem organizadinha com Artista, Ano, Capa, tudo nos conformes.
Para isso é possível tanto usar python com a lib mutagen que edita tags de MP3, FLAC e outros formatos, ou alguma solução como o Puddletag.
Artista/Álbum (Ano)/NN - Título.ext
Artista/Álbum (Ano)/D-NN - Título.ext (álbuns com mais de um disco)
Artista/Álbum (Ano)/cover.jpg
Pra mandar tudo pro servidor usei tar por ssh.
cd organizado && tar cf - . | ssh media.home 'tar xf - -C ~/music'
Streaming já funcionando:

Fiscalização Last.fm
Eu uso o Last há anos e não queria deixar de ‘scrobblar’ ouvindo essa biblioteca, afinal, a fiscalização Last.fm nunca deixa de trabalhar.
O Navidrome suporta a conexão com o Last.fm usando de uma conta de API que pode ser feita de forma bem simples pelo perfil da plataforma.


Depois de habilitar no meu perfil, usei um .env com as credenciais da API para não soltar no compose:
# /opt/navidrome/.env (chmod 600)
ND_LASTFM_APIKEY=
ND_LASTFM_SECRET=
No compose do Navidrome basta habilitar o Last e apontar para o arquivo com as keys.
environment:
ND_LASTFM_ENABLED: "true"
env_file:
- .env
Depois de reiniciar o container, é só voltar em Personal, ligar Scrobble to Last.fm e autorizar o Navidrome na janelinha do Last.fm. Essa configuração fica então habilitada em minha conta do Navi e linkada ao meu Last.

(1) Last.fm vinculado e (2) scrobble ativado.
Então só validei no web player do Navidrome o scrobble em tempo real lá no meu Last.fm:

(1) Tocando no Navidrome e (2) aparecendo no Last.fm como “Scrobbling now”.
Acesso de fora de casa
Eu queria ouvir minhas músicas também pela internet mas estando atrás de um NAT. A solução foi juntar o Tailscale com um domínio que eu já tinha no Cloudflare.
Hoje o Navidrome responde em https://music.noiseops.net com certificado válido, mas só para os meus dispositivos cadastrados no Tailscale. :)
Abaixo coloco um desenho para explicar melhor essa parte:

O truque é que o nome aponta pro IP do servidor dentro do Tailscale, um endereço que só existe na minha tailnet. Para conseguir o certificado sem expor nada, o Caddy prova ao Let’s Encrypt que o domínio é meu criando um registro TXT pela API do Cloudflare. É o chamado desafio DNS-01, e ele dispensa qualquer acesso vindo da internet.
Tailscale na VM
A instalação é um comando só, e o tailscale up mostra um link pra aprovar a máquina na sua conta.
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
Cloudflare
No painel do Cloudflare criei um token de API a partir do modelo Edit zone DNS, liberado só pra zona do meu domínio noiseops.net. Depois adicionei um registro A chamado music apontando pro IP do servidor no Tailscale, marcado como DNS only.

Caddy
O Caddy entrou no mesmo compose do Navidrome. A imagem oficial não vem com o módulo do Cloudflare, então precisei de um Dockerfile pequeno que compila o Caddy com ele usando o xcaddy.
# caddy/Dockerfile
FROM caddy:2-builder AS builder
RUN xcaddy build --with github.com/caddy-dns/cloudflare
FROM caddy:2
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
O Caddyfile inteiro tem oito linhas.
music.noiseops.net {
tls {
dns cloudflare {env.CF_API_TOKEN}
resolvers 1.1.1.1
}
encode zstd gzip
reverse_proxy navidrome:4533
}
E este é o serviço novo no compose.
caddy:
build: ./caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
env_file:
- .caddy.env # CF_API_TOKEN, chmod 600
volumes:
- "./Caddyfile:/etc/caddy/Caddyfile:ro"
- "./caddy/data:/data"
- "./caddy/config:/config"
depends_on:
- navidrome
O token do Cloudflare ficou num .caddy.env separado do .env do Last.fm. Assim cada container recebe só o conteúdo de que precisa. A renovação do certificado o Caddy faz sozinho.

Conclusão
No fim, a parte que deu mais trabalho não foi o Docker nem o proxy, mas sim organizar as tags de todas as músicas, para poder dormir em paz.
Vale a pena gastar esse tempo antes de subir as músicas, porque é isso que define se a biblioteca vai parecer organizada ou não. O resto são dois arquivos de configuração, alguns comandos no terminal e muuuita documentação.
Como o Caddy já está rodando, colocar outro serviço no mesmo servidor agora é também mais fácil. É só subir o container, criar mais um bloco no Caddyfile e mais um registro no Cloudflare. O próximo da fila provavelmente vai ser uma biblioteca de livros, ou talvez um servidor de fotos. :D