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.

Media center da 50 euro

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, con curl senza sessione è la risposta corretta.
  • docker ps non mostrava porte per Gerbera: normale con network_mode: host, il container usa direttamente le interfacce della macchina.
  • Un WARN sulla riga version: 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.

Dal CD alle mie orecchie

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