Ieri mattina mi sono svegliato e minimonio non c'era più. Non nel senso fisico: nel senso che si accendeva, mi guardava male, e mi piazzava davanti la schermata del BIOS rifiutandosi di avviare Ubuntu. Nella notte era crashato, punto. Il disco NVMe era ancora lì, il BIOS lo vedeva, ma del bootloader nemmeno l'ombra.
Ci avevo costruito sopra un piccolo agente che restituiva le info di sistema. Reinstallare tutto da zero è stato più veloce che giocare al chirurgo del GRUB in pyjama prima del caffè. Così ieri sera minimonio è ripartito da una fresh install di Ubuntu Server, e io ho pensato: visto che devo ricostruire, facciamola bene.
Obiettivo della serata: trasformare un mini-PC comprato usato a 50 euro (Celeron N5095, 7GB di RAM, un TB di disco) in un server multimediale per ascoltare i miei CD rippati — Oasis, Pink Floyd, e chi più ne ha più ne metta — senza accendere il laptop ogni volta. La catena finale: minimonio → UPnP → WiiM Ultra → cuffie Bluetooth.
Sono andato a dormire all'una di notte con The Wall nelle orecchie. Resoconto onesto di come ci sono arrivato, errori compresi.
Molti errori, tanto divertimento.
![]()
Il cast della rete di casa
minimonio non è solo: in casa convivono altri nodi.
- tuxedo: il mio laptop personale, da cui parte la sincronizzazione della musica.
- geekmonio: la macchina potente, con Ollama per l'inferenza locale.
- thinkmonio: un vecchio nodo edge.
Hardware eterogeneo, ruoli diversi, e un'unica conversazione con l'AI che li attraversa tutti. Stasera però il protagonista è il Celeron da 50 euro.
Le scelte architetturali
Filosofia: servizi piccoli, container isolati, tutto che sopravvive ai riavvii. Il collante è Docker.
- Navidrome: server musicale compatibile Subsonic, leggerissimo.
- Heimdall: dashboard per avere tutto sotto un tile.
- Gerbera: server DLNA/UPnP, per far parlare il server con lo Smart TV e il WiiM Ultra.
- Samba: per l'accesso diretto ai file.
Tutti con restart: unless-stopped, perché dopo un server che muore nel sonno non si scherza più con l'uptime.
La libreria arriva da tuxedo via rsync su SSH, lanciato dentro tmux — mai fidarsi di una connessione Wi-Fi per un terabyte di musica.
La serata, comunque, non è andata come il piano. La serata è andata meglio.
Gli inciampi: il cuore onesto del post
Se avessi seguito un tutorial sarebbe stato tutto lineare. Invece è stato un percorso a ostacoli. Che è il motivo per cui sto scrivendo questo post.
Il pacchetto che non esiste
Primo ostacolo, quasi subito: installare Docker Compose.
sudo apt install -y docker.io docker-compose-plugin
Error: Unable to locate package docker-compose-plugin
Il suggerimento veniva dall'AI, il nome era plausibile, il terminale diceva la verità: su Ubuntu il pacchetto si chiama docker-compose-v2. Prima lezione della serata, e l'AI l'ha imparata pure lei:
sudo apt install -y docker.io docker-compose-v2
Navidrome in crash-loop
Il container partiva, moriva, ripartiva, moriva. docker compose ps mostrava Restarting (1) in loop, e nei log sempre lo stesso messaggio:
level=fatal msg="Database could not be opened!" error="unable to open database file: no such file or directory"
Causa: la cartella host montata come /data non esisteva o non era scrivibile dall'utente 1000:1000 con cui gira Navidrome. I container non sono magia: un bind mount è solo una cartella, e i permessi sono i permessi.
mkdir -p ~/docker/navidrome ~/media/music
sudo chown -R 1000:1000 ~/docker/navidrome ~/media/music
docker compose up -d
Da lì Navidrome ha smesso di piangere.
Nota a margine per chi si trova nel mio stesso pasticcio: con Compose v2 il nome del container è prefissato dalla cartella del progetto (lorenzo-navidrome-1, non navidrome), quindi docker logs navidrome risponde con No such container. O usi il nome completo, o docker compose logs navidrome.
La porta ballerina di Gerbera
Al primo avvio, la web UI di Gerbera era finita su una porta semi-casuale (49494). Sembrava stabile, ma io non volevo giocare a “indovina la porta” a ogni reinstallazione — e visto come era andata la notte precedente, le reinstallazioni non erano un evento improbabile. Ho cambiato la porta nella config con sed e l'ho fissata lì per sempre. Osservazione, non certezza documentata: a me è capitato questo comportamento al primo avvio, ed è bastato a convincermi a scrivere la porta nella config a mano.
Stessa famiglia di problema, padre diverso: nano mi segnala config.xml come unwritable. Il file era stato creato dal processo dentro il container, che gira come root:
sudo chown -R 1000:1000 ~/docker/gerbera
Gerbera non ha nemmeno l'icona predefinita nella libreria di Heimdall, quindi il tile l'ho aggiunto a mano. Piccole cose, ma sono quelle che fanno la differenza tra “unattended” e “andiamo a vedere”.
Il bug bash dello script di sync
Il pezzo forte. Lo script sync_music.sh si impuntava con un errore sibillino:
Empty source arg specified.
rsync error: syntax or usage error (code 1) at main.c(1526) [sender=3.2.7]
Colpevole: una variabile "$DRY_RUN" vuota, che passava a rsync una stringa vuota come argomento — e rsync la interpretava come una sorgente mancante. Fix: trasformarla in array, che espanso vuoto diventa zero argomenti, come Dio comanda.
DRY_RUN=()
[[ "${1:-}" == "-n" ]] && DRY_RUN=(--dry-run)
# ...
rsync "${RSYNC_OPTS[@]}" "${DRY_RUN[@]}" "$SRC" ...
Secondo round, subito dopo:
./sync_music.sh: riga 47: DRY_RUN: variabile non assegnata
Con set -u attivo, il test [[ -n "$DRY_RUN" ]] usa la forma scalare dell'array che, se vuoto, risulta non assegnata. Bisognava testare la lunghezza:
[[ ${#DRY_RUN[@]} -gt 0 ]]
Bash ha un sistema di tipi insidioso. Le stesse due righe corrette le ho applicate con sed, perché a quell'ora della notte riscrivere a mano era chiedere troppo.
“Pink Floid” e l'album fantasma
Durante il sync scopro che una cartella battezzata “Pink Floid” — typo originale, anni di onorato servizio — vive su due macchine e si corregge solo su una. E l'album fantasma: “The Division Bell” è presente solo sul server.
Nessuno dei due è un bug. È la conseguenza logica di un rsync senza --delete: copia i nuovi file ma non cancella mai nulla, quindi non è un mirror. Nella versione finale dello script il --delete c'è, ma quella sera non c'era — e “Be Here Now” fantasma è la prova vivente. Morale doppia: se vuoi un mirror vero, dillo a rsync; e per la musica contano i tag ID3, non i nomi delle cartelle.
Gli errori falsi
Bonus della serata, gli errori che errori non erano:
- Navidrome rispondeva
HTTP 302: redirect alla pagina di login, concurlsenza sessione è la risposta corretta. docker psnon mostrava porte per Gerbera: normale connetwork_mode: host, il container usa direttamente le interfacce della macchina.- Un
WARNsulla rigaversion:obsoleta nel compose: solo un avviso, nei compose moderni non serve più.
Imparare a distinguere un errore vero da un errore apparente è metà del mestiere.
Il momento di gloria
Test finale: riavvio completo del server. Blackout simulato, per così dire. Checklist da rituale con docker ps e curl:
- Navidrome:
Up, HTTP 302 (redirect al login, tutto in regola). - Heimdall:
Up, HTTP 200. - Gerbera:
Up, HTTP 200.
Tre container su, tre risposte sane. Fully unattended: blackout, riavvii, aggiornamenti — dopo la notte del BIOS, questo minimo di dignità me la sono guadagnata.
Alle una di notte ero in letto con The Wall nelle cuffie, via WiiM, senza che il laptop fosse mai stato acceso. Il Celeron da 50 euro se l'era meritato.
![]()
Riflessioni sul lavorare con l'AI
La serata è stata un esperimento di pair-sysadmining: l'AI proponeva, io eseguivo, il terminale arbitrava. Quando l'AI ha suggerito un nome di pacchetto sbagliato, il terminale l'ha smascherata in un secondo. Gli errori dell'AI non sono un problema se il ciclo di feedback è stretto.
C'è una differenza tra un tutorial e una sessione reale: il tutorial mostra il percorso ideale, la sessione reale è un percorso a ostacoli. Entrambi insegnano qualcosa, ma solo il secondo insegna a diagnosticare. E diagnosticare — chiedersi “cambia con i riavvii?”, distinguere un 302 da un 500, capire perché una variabile vuota esplode — è la parte che rimane.
Buona musica, e buonanotte anche da qui.
Nota di trasparenza: l'AI del “pair-sysadmining” raccontato sopra si chiama Lumo, ed è l'assistente di Proton — stessa gente della mail cifrata. Mi ci sto trovando bene: oltre a darmi suggerimenti sbagliati sui nomi dei pacchetti (e a farseli rimbalzare dal terminale con sportività), ha una funzione canvas appena rilasciata che ha reso la stesura di questo post un lavoro fianco a fianco — appunti, bozze e revisioni a vista, con la musica che intanto partiva dai container. Se volete provarlo, qui c'è il mio referral link e se siete arrivati fino qui a leggere vi meritate pure i 10 euro di sconto che ci sono con il mio link.
docker-compose.yml
services:
navidrome:
image: deluan/navidrome:latest
user: 1000:1000
ports:
- "4533:4533"
volumes:
- /home/lorenzo/media/music:/music
- /home/lorenzo/docker/navidrome:/data
environment:
- ND_LOGLEVEL=info
- ND_SCANSCHEDULE=1h
- ND_SESSIONTIMEOUT=168h
restart: unless-stopped
heimdall:
image: linuxserver/heimdall
ports:
- "8080:80"
volumes:
- /home/lorenzo/docker/heimdall:/config
restart: unless-stopped
gerbera:
image: gerbera/gerbera
container_name: gerbera
network_mode: host
volumes:
- /home/lorenzo/media/music:/content:ro
- /home/lorenzo/docker/gerbera/config:/var/run/gerbera
restart: unless-stopped
sync_music.sh
#!/bin/bash
# sync_music.sh — Sincronizza la musica da Tuxedo a Minimonio (one-way, rsync via SSH)
# Uso: ./sync_music.sh [-n]
# -n : dry-run (mostra cosa farebbe senza copiare nulla)
set -euo pipefail
# ---------- CONFIGURAZIONE ----------
SRC="/home/lorenzo/Musica/" # Cartella sorgente sul Tuxedo (NOTA la barra finale!)
DST_USER="lorenzo"
DST_HOST="192.168.1.201" # IP di minimonio
DST_PATH="media/music/" # Destinazione relativa alla home su minimonio
LOG_DIR="$HOME/logs"
LOG_FILE="$LOG_DIR/sync_music_$(date +%Y%m%d_%H%M%S).log"
KEEP_LOGS=10 # Numero di log vecchi da conservare
LOCKFILE="/tmp/sync_music.lock"
# ------------------------------------
DRY_RUN=()
[[ "${1:-}" == "-n" ]] && DRY_RUN=(--dry-run)
# --- Protezione anti-doppioni ---
if [[ -f "$LOCKFILE" ]] && kill -0 "$(cat "$LOCKFILE")" 2>/dev/null; then
echo "❌ Un'altra sincronizzazione è già in corso (PID $(cat "$LOCKFILE")). Esco."
exit 1
fi
echo $$ > "$LOCKFILE"
trap 'rm -f "$LOCKFILE"' EXIT
mkdir -p "$LOG_DIR"
# --- Controllo sorgente ---
if [[ ! -d "$SRC" ]]; then
echo "❌ Cartella sorgente $SRC non trovata. Modifica SRC nello script."
exit 1
fi
MP3_COUNT=$(find "$SRC" -type f \( -iname '*.mp3' -o -iname '*.flac' -o -iname '*.m4a' \) | wc -l)
if [[ "$MP3_COUNT" -eq 0 ]]; then
echo "⚠️ Nessun file musicale trovato in $SRC — controllo prima di continuare."
read -rp "Procedo comunque? [s/N] " ans
[[ "$ans" == "s" ]] || exit 0
fi
echo "🎵 Sincronizzazione musica: $SRC -> $DST_USER@$DST_HOST:$DST_PATH"
echo " File musicali in sorgente: $MP3_COUNT"
[[ ${#DRY_RUN[@]} -gt 0 ]] && echo " (DRY-RUN: nessun file verrà realmente copiato)"
echo ""
# --- Raggiungibilità ---
if ! ping -c1 -W2 "$DST_HOST" > /dev/null 2>&1; then
echo "❌ $DST_HOST non raggiungibile. Controlla la rete."
exit 1
fi
# --- Sincronizzazione ---
# -a preserva permessi, date, struttura
# -v verbose
# -h dimensioni leggibili
# --delete cancella su destinazione ciò che è stato rimosso in sorgente (mirror vero)
# --info=progress2 barra di avanzamento globale
# --exclude salta file temporanei e nascosti
RSYNC_OPTS=(-avh --info=progress2 --stats
--exclude='.*' --exclude='Thumbs.db' --exclude='desktop.ini'
--exclude='*.tmp')
echo " Nota: mirroring ATTIVO (--delete). Commentala per conservare i file rimossi."
echo ""
rsync "${RSYNC_OPTS[@]}" "${DRY_RUN[@]}" "$SRC" "$DST_USER@$DST_HOST:$DST_PATH" 2>&1 | tee "$LOG_FILE"
# --- Pulizia log vecchi ---
ls -1t "$LOG_DIR"/sync_music_*.log 2>/dev/null | tail -n +"$((KEEP_LOGS + 1))" | xargs -r rm --
echo ""
echo "✅ Sincronizzazione completata. Log: $LOG_FILE"
# --- Suggerimento ---
if [[ ${#DRY_RUN[@]} -eq 0 ]]; then
echo "ℹ️ Navidrome re-scansiona la libreria entro 1 ora (ND_SCANSCHEDULE=1h)."
echo " Per forzare subito lo scan: apri http://$DST_HOST:4533 e vai su ⚙ → Scan Library."
fi
“La tecnologia libera non è solo codice, è la libertà di capire cosa c'è sotto il cofano.”
👤 Lorenzo | ItsMeLarry 🧪 Esperimenti AI & Open Source | 🕹️ Retro Gaming & Graphic Novel 🎸 Rock, Folk, Jazz, Punk | 🔒 Privacy First 📍 Toscana / Digitale
🔗 noblogo.org | 🐙 Codeberg