Un aggiornamento di Docker Engine su un server in produzione
Aggiornare Docker Engine su un server che esegue attivamente più di cento container non è un compito che premia l'improvvisazione. Quando è arrivato il momento di passare da Docker Engine 28.1 a 28.2 e da Docker Compose 2.35 a 2.36 sul nostro server di produzione Hetzner, l'abbiamo affrontato con la stessa disciplina che applicheremmo a una migrazione di database: una checklist di preparazione scritta, un percorso di rollback testato, una sequenza di esecuzione precisa e una fase di verifica post-aggiornamento prima di chiudere la finestra di manutenzione.
Ciò che segue è un resoconto completo di come l'abbiamo affrontato — il ragionamento alla base di ogni passaggio, i comandi che abbiamo effettivamente eseguito e a cosa prestare attenzione quando il numero di container è abbastanza alto da far sì che una politica di riavvio configurata male possa provocare una cascata di problemi e complicare il ripristino.
Perché questo aggiornamento richiedeva una procedura operativa formale
Docker Engine 28.2 ha aggiunto opzioni di rete bridge e un’opzione del daemon per il routing diretto verso i container, e ha rimosso il supporto per il pull di immagini legacy schema 1. Nella stessa finestra di manutenzione, il plugin Compose è passato da 2.35.1 a 2.36.0. È una release minore, ma vale la pena testare qualsiasi cambiamento nell’avvio di uno stack, in particolare i blocchi depends_on con condition: service_healthy.
Su un server in cui un singolo docker-compose.yml potrebbe governare dodici servizi interdipendenti — database, reverse proxy, container applicativi, worker in background — un cambiamento di comportamento non segnalato nella risoluzione delle dipendenze è esattamente il tipo di problema che non emerge immediatamente. Emerge alle 03:00 quando un healthcheck non si attiva mai e un container dipendente entra in un ciclo senza fine.
La risposta pratica è la preparazione, non la cautela fine a se stessa.
Checklist di preparazione
Prima di toccare un singolo pacchetto, abbiamo seguito la seguente checklist. Ogni elemento ha una ragione.
1. Fare l'inventario di tutti i container in esecuzione e delle loro politiche di riavvio.
docker ps --format "table {{.Names}} {{.Status}} {{.Image}}" | sort
docker inspect $(docker ps -q) --format '{{.Name}} restart={{.HostConfig.RestartPolicy.Name}}' | sort
Questo fornisce un riferimento iniziale. I container con restart: always tenteranno di riavviarsi automaticamente dopo il riavvio del daemon Docker — che è il comportamento desiderato, ma solo se i file Compose e le immagini sottostanti sono in uno stato verificato e funzionante. Qualsiasi container in un ciclo continuo di arresti e riavvii al momento dell'aggiornamento sarà ancora in un ciclo continuo di arresti e riavvii successivamente, ed è meglio saperlo ora.
2. Validare tutti i file Compose prima dell’aggiornamento.
docker compose config --quiet 2>&1 | grep -i warning
Esegui questo comando in ogni directory che contiene un docker-compose.yml. Il comando usa la versione di Compose attualmente installata, quindi rileva gli avvisi esistenti senza dimostrare la compatibilità con la versione di destinazione; per verificarla, testa quest’ultima in un ambiente di preproduzione. Le release attuali di Compose segnalano la chiave obsoleta version: all’inizio dei file Compose (viene ignorata), mentre la vecchia direttiva links: non è più necessaria, perché i servizi su una rete condivisa si raggiungono per nome. Gli avvisi possono diventare errori nelle release successive; vale la pena risolverli ora.
3. Verificare lo spazio disponibile su disco.
df -h /var/lib/docker
docker system df
L'aggiornamento scaricherà un nuovo shim containerd e sostituirà il binario Docker Engine. Il vecchio binario e le sue dipendenze non vengono sempre eliminati automaticamente. Su un server che è in esecuzione da un anno, docker system df spesso rivela diversi gigabyte di layer di immagini recuperabili. Elimina questi layer prima dell’aggiornamento, non durante.
docker system prune -f --volumes
Usa --volumes solo se sei certo che nessun volume anonimo contenga dati che ti servono. Sul nostro server, tutti i dati persistenti sono montati da volumi con nome, dichiarati esplicitamente — quindi era sicuro.
4. Esportare la versione corrente di Docker Engine e di Compose in un file di riferimento.
docker version > /root/docker-pre-upgrade.txt
docker compose version >> /root/docker-pre-upgrade.txt
docker ps -a >> /root/docker-pre-upgrade.txt
dpkg-query -W docker-ce docker-ce-cli containerd.io docker-compose-plugin >> /root/docker-pre-upgrade.txt
Questo richiede dieci secondi e fornisce un riferimento di rollback inequivocabile se qualcosa va storto e devi identificare quali container erano in esecuzione al momento dell'aggiornamento.
5. Avvisare i sistemi a valle.
Il nostro server esegue uno stack Supabase gestito sulla nostra infrastruttura e diverse istanze di automazione dei flussi di lavoro n8n. Qualsiasi webhook che si attivi durante un ciclo di riavvio dei container fallirà e, se il mittente non ritenta l’invio, l’evento può andare perso senza che nessuno se ne accorga. Abbiamo impostato una finestra di manutenzione nel nostro strumento di monitoraggio della disponibilità e disabilitato i webhook in entrata in n8n per tutta la durata della manutenzione.
Il piano di rollback
Un piano di rollback è utile solo se si è stabilito in anticipo quale condizione lo attiva. Il nostro era semplice: se qualsiasi container che era in esecuzione prima dell'aggiornamento non è in esecuzione dieci minuti dopo il completamento dell'aggiornamento e non può essere recuperato con un docker compose up -d, si riporta il binario Docker Engine alla versione precedente.
Fare il rollback di Docker Engine su un sistema Debian significa vincolare il pacchetto alla versione precedente. Abbiamo annotato la stringa di versione esatta prima di iniziare:
apt-cache policy docker-ce docker-ce-cli containerd.io docker-compose-plugin
Output sul nostro server prima dell'aggiornamento:
docker-ce:
Installed: 5:28.1.1-1~debian.12~bookworm
Candidate: 5:28.2.2-1~debian.12~bookworm
docker-compose-plugin:
Installed: 2.35.1-1~debian.12~bookworm
Candidate: 2.36.0-1~debian.12~bookworm
Il comando di rollback, nel caso fosse necessario:
CONTAINERD_PREV=$(awk '$1 == "containerd.io" {print $2}' /root/docker-pre-upgrade.txt)
apt-get install -y --allow-downgrades docker-ce=5:28.1.1-1~debian.12~bookworm docker-ce-cli=5:28.1.1-1~debian.12~bookworm containerd.io="${CONTAINERD_PREV}" docker-compose-plugin=2.35.1-1~debian.12~bookworm
Il comando fissa anche containerd.io alla versione salvata nel file di riferimento al passaggio 4 e passa --allow-downgrades, perché apt-get -y si interrompe invece di installare versioni precedenti dei pacchetti in modalità non interattiva.
Abbiamo mantenuto questo comando in un file di testo sul server in /root/docker-rollback.sh con permessi di esecuzione, pronto per essere eseguito senza digitare sotto pressione.
Il processo di aggiornamento
L'aggiornamento effettivo è semplice una volta completata la preparazione. La sequenza è importante: prima aggiornare l'indice dei pacchetti, verificare cosa verrà installato, poi installare.
Passaggio 1: Aggiornare l'indice apt solo per il repository Docker.
apt-get update -o Dir::Etc::sourcelist="sources.list.d/docker.list" -o Dir::Etc::sourceparts="-" -o APT::Get::List-Cleanup="0"
Questo limita l'aggiornamento al repository Docker piuttosto che aggiornare tutte le sorgenti. Su un server di produzione, un aggiornamento di pacchetti non intenzionale da una sorgente non correlata durante una finestra di manutenzione Docker è una variabile di cui non si ha bisogno.
Passaggio 2: Confermare le versioni candidate.
apt-cache policy docker-ce docker-ce-cli containerd.io docker-compose-plugin
Verifica che la versione candidata corrisponda a quella attesa prima di procedere. Se il candidato mostra una versione più recente di quella testata nell’ambiente di preproduzione, fermati e valuta.
Passaggio 3: Installare.
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
Questo fermerà il daemon Docker, sostituirà i binari e riavvierà il daemon. I container con restart: always verranno riavviati dal daemon al riavvio. L’intervallo durante il quale i container non sono in esecuzione è tipicamente inferiore a trenta secondi su un server di dimensioni ragionevoli — anche se varia a seconda di quanto tempo impiega containerd per inizializzarsi.
Osserva il riavvio in un secondo terminale:
watch -n 2 'docker ps --format "table {{.Names}} {{.Status}}" | sort'
Passaggio 4: Verificare le versioni installate.
docker version
docker compose version
Sul nostro server, l'output ha confermato:
Docker Engine: 28.2.2
Docker Compose: v2.36.0
Docker Compose segue una numerazione delle versioni indipendente da Docker Engine: la versione del pacchetto docker-compose-plugin (qui 2.36.0) è la versione di Compose ed è quella registrata dall’indice apt.
Verifica dopo l’aggiornamento
Dieci minuti dopo l'aggiornamento, abbiamo eseguito una verifica strutturata. Questa verifica non è facoltativa — il riavvio del daemon riavvierà i container, ma non garantisce che i container che dipendono dagli healthcheck li abbiano effettivamente superati.
Verificare che tutti i container siano nello stato atteso.
docker ps -a --format "table {{.Names}} {{.Status}} {{.RunningFor}}" | sort
Qualsiasi container che mostra Restarting o Exited richiede un’indagine immediata. Sul nostro server, un container — un servizio di analisi Supabase — mostrava Restarting (1). Era in un ciclo di arresti e riavvii poco frequenti anche prima dell'aggiornamento; l'aggiornamento non lo ha causato, ma è emerso più visibilmente nella scansione post-aggiornamento. È proprio il tipo di problema preesistente che l’inventario prima dell’aggiornamento serve a rilevare.
Verificare che gli healthcheck vengano superati.
docker inspect $(docker ps -q) --format '{{.Name}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' | grep -v "health=none" | sort
I container senza healthcheck espliciti non hanno uno stato di salute; a seconda della versione di Docker, un {{.State.Health.Status}} senza controllo mostra <no value> oppure interrompe l’intero comando con un errore del template. Il template verifica quindi prima .State.Health, e grep esclude i container che non lo hanno. Quelli che contano sono quelli da cui dipendono gli altri container tramite condition: service_healthy.
Eseguire una verifica della configurazione Compose su tutti gli stack.
for dir in /opt/*/; do
if [ -f "${dir}docker-compose.yml" ]; then
echo "--- $dir ---"
docker compose -f "${dir}docker-compose.yml" config --quiet 2>&1
fi
done
Questo rileva tutte le direttive del file Compose che la nuova versione di Compose tratta come avvisi o errori. Sul nostro server, tre stack hanno emesso l'avviso di deprecazione della chiave version. Sono avvisi puramente formali, ma vale la pena risolverli nel prossimo ciclo di manutenzione.
Controllare i log del daemon Docker alla ricerca di errori.
journalctl -u docker --since "1 hour ago" | grep -i -E "error|warn|fatal"
Un aggiornamento pulito non produce errori nel log del daemon. Vale la pena annotare gli avvisi sulla configurazione deprecata, senza intervenire immediatamente.
Confermare la connettività di rete tra i container.
docker network ls
docker network inspect bridge --format '{{range .Containers}}{{.Name}} {{end}}'
Il secondo comando mostra solo la rete bridge predefinita. Ripetilo per ogni rete personalizzata elencata da docker network ls per confermare che le reti bridge personalizzate includano ancora i container previsti dopo il riavvio del daemon, e testa almeno una connessione reale tra servizi dipendenti.
Che cosa abbiamo osservato
L'aggiornamento è stato completato in meno di due minuti. Tutti i container che erano in esecuzione prima dell'aggiornamento erano in esecuzione dopo. Il cambiamento di Compose che abbiamo notato più immediatamente è stato un formato di output --dry-run più pulito — la nuova versione produce output strutturato e colorato che rende più facile verificare cosa farà effettivamente un docker compose up prima di eseguirlo.
L'ordinamento dell'healthcheck depends_on si è comportato in modo identico a Compose 2.35 sui nostri stack. Era atteso — avevamo validato i file Compose in anticipo — ma è stato rassicurante confermarlo.
Una constatazione sostanziale: Compose non consente di combinare il campo container_name con scale, perché i nomi dei container devono essere univoci. Avevamo uno stack che usava entrambi — un vecchio residuo da prima che passassimo a una corretta gestione delle repliche. Compose ha emesso un errore chiaro su docker compose up per quello stack. È bastata una modifica di una riga per rimuovere il campo container_name ridondante.
Approfondimenti correlati
- Distribuire applicazioni React in produzione: configurazione Docker completa con il reverse proxy Traefik
- Costruire uno stack di sviluppo multi-tenant con Docker
- Reverse proxy Traefik: guida completa al self-hosting per HTTPS e automazione SSL
- Self-hosting di n8n su Hetzner Cloud: tutorial completo di configurazione Docker