Aggiornamento automatico dei container Docker in produzione senza downtime

Aggiornamento automatico dei container Docker in produzione senza downtime

Il 3 febbraio 2026 ho ricevuto alle 22:47 una chiamata dal responsabile IT di un'azienda del settore e-commerce B2B con circa 55 dipendenti e un fatturato annuale nell'ordine dei 9 milioni di euro, appoggiata su un Hetzner AX42 (Ryzen 5 3600, 64 GB RAM, 2×NVMe da 512 GB in RAID 1) con otto container Docker in produzione - Nginx reverse proxy, due worker PHP-FPM 8.2, MySQL 8.0, Redis 7, un container Laravel Horizon, un container Node.js per il BFF e un piccolo servizio Python per l'elaborazione di bolle di accompagnamento. Alle 22:30 Watchtower aveva scaricato automaticamente l'immagine latest del container Node.js, fermato il vecchio e avviato il nuovo. Il nuovo container si avviava, restava "Up 2 seconds", moriva, ripartiva, moriva di nuovo. Il BFF era fuori servizio da diciassette minuti, il frontend mostrava errori 502, il monitoraggio esterno pingava il titolare ogni due minuti. Nessun rollback automatico, nessun alert proattivo prima dell'incidente, nessuna procedura documentata per tornare indietro. Quindici minuti dopo avevo ripristinato manualmente l'immagine precedente con docker tag e il sito era tornato online. Il danno stimato in mancati ordini durante i diciassette minuti di downtime: circa 2.800 euro, concentrati in quella che per un B2B è una fascia oraria commercialmente morta. Se la stessa cosa fosse successa alle 11 del mattino del martedì successivo, avremmo parlato di cifre di un ordine di grandezza superiore.

Questo articolo è il distillato di come ho riprogettato la pipeline di aggiornamento automatico dei container Docker per quel cliente e per quattro altre PMI simili negli ultimi dodici mesi, applicando un pattern che separa nettamente tre responsabilità: rilevamento dell'aggiornamento, validazione prima dell'esecuzione, rollback automatico in caso di fallimento. Watchtower in configurazione di default è la promessa comoda dell'"aggiornamento automatico senza pensieri"; in produzione, senza layer aggiuntivi, è uno dei modi più prevedibili per generare incidenti di indisponibilità. Il principio operativo che ripeto a ogni cliente: il 60% degli incidenti di sicurezza che gestisco nasce da software non aggiornato, il 15% degli incidenti di indisponibilità nasce da software aggiornato male. La risposta non è scegliere fra i due rischi, è costruire una pipeline che minimizzi entrambi.

Cosa succede davvero quando Watchtower aggiorna un container in produzione senza health check?

Succede esattamente quello che è successo al cliente dell'incipit: Watchtower rileva un digest nuovo sul registry, scarica l'immagine, ferma il container vecchio, avvia il nuovo e da quel momento si disinteressa del risultato. Il progetto Watchtower non ha mai supportato il rollback driven da health check, e lo stato corrente lo rende ancora più chiaro: il repository ufficiale containrrr/watchtower è stato archiviato dall'autore a dicembre 2025 in modalità read-only, con ultima release stabile v1.7.1 e issue/discussioni bloccate. La stessa documentazione del progetto sconsiglia esplicitamente l'uso in ambienti commerciali o di produzione. Un fork attivo mantenuto da Nicholas Fedor continua lo sviluppo, ma il modello architetturale di fondo non cambia: Watchtower è un updater, non un orchestratore con rollback.

La differenza pratica si misura in poche righe di docker events. Dopo un aggiornamento Watchtower di default vedi, in sequenza: image pull, container create, container stop (sul vecchio), container start (sul nuovo). Nessun evento di verifica, nessun gate di validazione, nessun marker "unhealthy → rollback". Se il nuovo container fallisce l'avvio in crashloop perché la nuova versione ha introdotto un bug di configurazione, un mismatch di migrazione database o semplicemente una regressione di compatibilità, Watchtower ha già dimenticato l'immagine precedente e non ha alcuna logica per riportarla in servizio.

La lezione operativa è che la parola "automatico" nella frase "aggiornamento automatico dei container" deve includere tre fasi, non una. Scaricare l'immagine è automatico, ma non basta. Avviare il nuovo container è automatico, ma non basta. La terza fase, validare che il nuovo container stia effettivamente servendo traffico correttamente e, se non lo sta facendo, ripristinare il precedente, è quella che Watchtower da solo non ti dà. Ed è esattamente la fase che in un discorso più ampio sulla sicurezza dei container Docker per applicazioni PHP in produzione ho descritto come il confine fra un container management "da sviluppatore" e uno "da produzione".

Watchtower in modalità monitor-only: la configurazione che raccomando come primo gradino

Il primo intervento strutturale che eseguo sui clienti che vogliono l'automazione degli aggiornamenti ma non hanno ancora investito in una pipeline CI/CD completa è passare Watchtower in modalità monitor-only. In questa modalità, Watchtower continua a controllare il registry periodicamente per rilevare nuove immagini, ma non esegue alcuna azione: si limita a inviare una notifica al canale configurato. L'aggiornamento vero viene eseguito a mano in finestra pianificata, oppure da uno script controllato che include health check e rollback.

La configurazione Docker Compose che applico come baseline è questa, con commenti in italiano che spiegano ogni scelta:

# docker-compose.watchtower.yml - monitor-only in produzione
services:
  watchtower:
    image: containrrr/watchtower:1.7.1
    container_name: watchtower
    restart: unless-stopped
    volumes:
      # accesso al socket docker per leggere i digest
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      # modalità monitor-only: rileva ma non aggiorna
      WATCHTOWER_MONITOR_ONLY: "true"
      # controlla ogni 6 ore, non più spesso per ridurre rumore
      WATCHTOWER_SCHEDULE: "0 0 */6 * * *"
      # opt-in esplicito: solo i container con label sono considerati
      WATCHTOWER_LABEL_ENABLE: "true"
      # notifica su Telegram del team ops
      WATCHTOWER_NOTIFICATIONS: shoutrrr
      WATCHTOWER_NOTIFICATION_URL: "telegram://BOT_TOKEN@telegram?chats=-100123456789"
      # log strutturato per aggregarli in Loki/Grafana
      WATCHTOWER_LOG_FORMAT: "json"
      TZ: "Europe/Rome"

Tre scelte meritano una spiegazione. Prima: WATCHTOWER_LABEL_ENABLE=true impone il pattern opt-in - solo i container che hanno esplicitamente la label com.centurylinklabs.watchtower.enable=true vengono considerati. Con il pattern inverso (opt-out) qualunque container nuovo lanciato sul server finisce in perimetro di aggiornamento automatico per default, e nella pratica questo genera il 90% degli incidenti da "ma non sapevo che anche MySQL fosse in quella lista". Seconda: il socket Docker è montato in read-only (:ro) perché in monitor-only mode Watchtower non deve scrivere nulla; questa piccola differenza rende impossibili escalation di privilegi tramite il container Watchtower stesso nel caso in cui la sua immagine sia compromessa - un pattern difensivo documentato in dettaglio nelle raccomandazioni OWASP sui Docker socket proxy. Terza: la frequenza a 6 ore è un compromesso calibrato sui miei clienti, che non hanno requisiti di patch di sicurezza sub-day e preferiscono finestre di notifica prevedibili a controlli ossessivi ogni 5 minuti.

Stai cercando un Consulente Informatico esperto per mettere in sicurezza la pipeline di deploy dei tuoi container Docker? Nel mio profilo professionale trovi l'esperienza concreta su orchestrazione, CI/CD e automazione sicura per PMI che girano su Hetzner, OVH, Digital Ocean e Contabo.

La pipeline completa: script di aggiornamento con smoke test e rollback

Dopo Watchtower in monitor-only, il secondo gradino è lo script che esegue effettivamente l'aggiornamento quando il team decide di applicarlo. Lo script è volutamente semplice - bash, niente framework - perché deve essere leggibile a colpo d'occhio dal responsabile IT del cliente alle 23 di un venerdì, non richiedere competenze avanzate di Ansible o Terraform per essere mantenuto. La struttura è sempre la stessa: tag dell'immagine corrente come rollback, pull della nuova, avvio con docker compose up -d, attesa della salute del container, smoke test applicativo, e ripristino della precedente se uno qualunque dei gate fallisce.

#!/usr/bin/env bash
# /opt/ops/docker-safe-update.sh - aggiornamento con rollback automatico
# Uso: ./docker-safe-update.sh <service_name> <new_image_tag>
set -euo pipefail

SERVICE="${1:?serve il nome del servizio}"
NEW_TAG="${2:?serve il tag della nuova immagine}"
COMPOSE_FILE="/opt/stack/docker-compose.yml"
HEALTH_TIMEOUT=120     # secondi massimi di attesa per healthcheck
SMOKE_URL="http://127.0.0.1:8080/health"
LOG="/var/log/docker-update-${SERVICE}-$(date +%Y%m%d-%H%M%S).log"

log() { echo "[$(date -u +%H:%M:%S)] $*" | tee -a "$LOG"; }

# 1) snapshot della versione corrente per il rollback
CURRENT_IMAGE=$(docker inspect --format '{{.Config.Image}}' "$SERVICE")
log "Immagine corrente: $CURRENT_IMAGE"
docker tag "$CURRENT_IMAGE" "${SERVICE}:rollback-$(date +%Y%m%d-%H%M%S)"

# 2) pull della nuova immagine PRIMA di toccare il container attivo
log "Pull della nuova immagine ${NEW_TAG}..."
docker pull "$NEW_TAG" || { log "Pull fallito, aborting"; exit 1; }

# 3) aggiornamento via docker compose
export NEW_IMAGE="$NEW_TAG"
docker compose -f "$COMPOSE_FILE" up -d --no-deps "$SERVICE"

# 4) attesa della salute del container tramite HEALTHCHECK Docker
log "Attesa di health status healthy..."
elapsed=0
while [ $elapsed -lt $HEALTH_TIMEOUT ]; do
    status=$(docker inspect --format '{{.State.Health.Status}}' "$SERVICE" 2>/dev/null || echo "none")
    if [ "$status" = "healthy" ]; then
        log "Container healthy dopo ${elapsed}s"
        break
    fi
    sleep 3
    elapsed=$((elapsed + 3))
done

if [ "$status" != "healthy" ]; then
    log "HEALTHCHECK fallito, rollback in corso..."
    docker compose -f "$COMPOSE_FILE" stop "$SERVICE"
    export NEW_IMAGE="$CURRENT_IMAGE"
    docker compose -f "$COMPOSE_FILE" up -d --no-deps "$SERVICE"
    exit 2
fi

# 5) smoke test applicativo oltre la salute del runtime
log "Smoke test verso $SMOKE_URL..."
if ! curl -fsS --max-time 10 "$SMOKE_URL" > /dev/null; then
    log "Smoke test fallito, rollback in corso..."
    docker compose -f "$COMPOSE_FILE" stop "$SERVICE"
    export NEW_IMAGE="$CURRENT_IMAGE"
    docker compose -f "$COMPOSE_FILE" up -d --no-deps "$SERVICE"
    exit 3
fi

log "Aggiornamento completato con successo. Nuova immagine: $NEW_TAG"

Tre dettagli dello script meritano di essere commentati. Il primo è il docker tag iniziale: non mi affido alla cache locale di Docker per il rollback, perché la garbage collection configurata su molti server (docker system prune settimanale) potrebbe cancellarla. Un tag esplicito con timestamp è una garanzia in più che l'immagine precedente sia recuperabile per almeno sette giorni, anche se il registry remoto diventa irraggiungibile. Il secondo è la separazione netta fra HEALTHCHECK del container (che verifica che il processo interno sia vivo e risponda al suo health endpoint locale) e smoke test applicativo (che verifica che il container sia effettivamente raggiungibile attraverso il reverse proxy e serva richieste reali). I due falliscono per ragioni diverse: il primo cattura crashloop e deadlock interni, il secondo cattura misconfiguration di rete o di proxy che il container non può rilevare da solo. Il terzo è l'uso di --no-deps: aggiorno solo il servizio target, senza toccare i container dipendenti, per evitare cascate di riavvii non previste.

Un pattern alternativo che uso quando il cliente ha già una pipeline CI/CD strutturata è quello dell'aggiornamento tramite merge request su un repo Git che contiene i tag delle immagini: Renovate Bot apre automaticamente PR con il nuovo digest e il pipeline deploya dopo il merge. Questo approccio - che ho descritto in dettaglio nel contesto di un flusso di automazione del deploy di applicazioni Laravel su VPS Hetzner e OVH - vince quando il team ha bisogno di audit trail e approvazioni formali, perdendo però immediatezza rispetto allo script bash. La scelta è sempre un trade-off fra velocità e governance; per PMI con team da 3-8 persone lo script bash è quasi sempre la risposta giusta, per aziende più strutturate Renovate o flussi equivalenti con ArgoCD diventano più sensati.

Docker HEALTHCHECK: il gate che rende il rollback davvero automatico

Il linchpin dell'intera pipeline è il Dockerfile del container, perché senza HEALTHCHECK scritto bene nessuno script di rollback può funzionare. La direttiva HEALTHCHECK di Docker, documentata ufficialmente nella reference del Dockerfile, permette di dichiarare un comando che il daemon Docker esegue periodicamente per determinare se il container è in stato healthy, unhealthy o starting. Senza questa direttiva, docker inspect restituisce none nel campo State.Health.Status, e lo script di rollback sopra non ha modo di distinguere un container sano da uno in crashloop silenzioso.

La configurazione che uso come pattern di default in ogni Dockerfile di produzione è questa, per un'applicazione PHP-FPM dietro Nginx:

# Dockerfile - pattern healthcheck per container PHP-FPM
FROM php:8.2-fpm-alpine

# installazione delle estensioni e configurazione applicativa
RUN docker-php-ext-install pdo_mysql opcache

COPY ./app /var/www/html
WORKDIR /var/www/html

# endpoint di salute: il file verrà servito da nginx sullo stesso pod
COPY ./docker/health.php /var/www/html/public/health.php

# il comando di healthcheck verifica tre cose:
# 1) php-fpm risponde sulla porta interna
# 2) la connessione al DB è sana
# 3) redis risponde al ping
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 \
    CMD php /var/www/html/docker/healthcheck.php || exit 1

EXPOSE 9000
CMD ["php-fpm", "-F"]

Il valore di --start-period=30s è il parametro che i developer novizi di Docker configurano male più spesso. Questa direttiva dice al daemon di ignorare i fallimenti di healthcheck nei primi trenta secondi dall'avvio, considerandoli "starting" e non "unhealthy". Senza questo parametro, un container che impiega 25 secondi a fare bootstrap completo (caricamento di configurazioni, connessione a Redis, warm-up della cache Eloquent) viene marcato come unhealthy già al primo check dopo i primi 15 secondi, e lo script di rollback lo abbatte pensando che sia in crashloop. Con --start-period=30s, invece, il container ha un buffer ragionevole prima che i suoi check inizino a contare davvero. Il valore va calibrato sul tempo di bootstrap osservato sul container specifico; su un Laravel complesso con molte service provider può arrivare a 60-90 secondi, su un servizio Go compilato staticamente resta tipicamente sotto i 5 secondi.

Il file healthcheck.php che richiamo dal container esegue in sequenza tre verifiche: una query SELECT 1 sul database con timeout a 2 secondi, un PING a Redis con timeout a 1 secondo, e una verifica che il filesystem scrivibile (storage/logs) sia effettivamente scrivibile. Se tutti e tre passano, esce con codice 0; altrimenti esce con codice 1 e Docker conta un fallimento. Dopo tre fallimenti consecutivi (--retries=3), il container passa in stato unhealthy e lo script di rollback lo rileva. Questo design è volutamente conservativo: un healthy → unhealthy reale viene rilevato in 45 secondi netti (tre retry × 15 secondi di intervallo), mentre un singolo hiccup di rete di 5 secondi non genera rollback perché il retry successivo riprende. Sul cliente e-commerce dell'incipit, dopo l'introduzione di questo pattern abbiamo avuto in quattro mesi 23 aggiornamenti riusciti con rollback mai attivato, due aggiornamenti con rollback attivato correttamente (uno per una migrazione database che andava eseguita prima dell'update del container, uno per un typo in una variabile d'ambiente), e zero downtime percepiti dagli utenti.

Quando Watchtower non basta: Kubernetes, k3s e la soglia di complessità

Esiste una soglia oltre la quale nemmeno la pipeline descritta sopra è più sufficiente, e il messaggio onesto da dare ai clienti è che quella soglia non è così lontana come vorrebbero credere. Se hai più di 12-15 container in produzione, se gestisci rolling update con canary release o blue/green deployment, se hai requisiti di multi-AZ o alta disponibilità geografica, lo script bash inizia a mostrare la corda e il suo manutenzione diventa un lavoro a tempo pieno. In quei casi la risposta ingegneristica corretta è un orchestratore vero. K3s e MicroK8s sono due distribuzioni Kubernetes leggere, specificamente pensate per chi non vuole i costi operativi di un cluster managed su cloud provider ma ha bisogno della primitive di rolling update controllato, del readiness probe come gate di promozione, e del rollback nativo via kubectl rollout undo.

In un progetto di migrazione infrastrutturale per una PMI del settore logistica ho appena completato un passaggio da 22 container gestiti con docker-compose e Watchtower a un cluster k3s a tre nodi su Hetzner CAX41, con Traefik come ingress, Longhorn per storage persistente e Argo CD per il deploy dichiarativo. Il beneficio concreto non è stato la "features di Kubernetes" - che per un'azienda di quelle dimensioni è quasi sempre overkill - ma la capacità del cluster di continuare a servire traffico durante un aggiornamento, di attendere che il nuovo pod passi il readiness probe prima di togliere traffico al vecchio, e di ripristinare con un singolo comando la versione precedente in caso di problema rilevato post-deploy. Ho approfondito quando la soglia di complessità giustifica il passaggio in un articolo dedicato al vantaggio di Kubernetes come orchestratore di container in contesti aziendali, che è il seguito naturale di questo per chi sta valutando se Watchtower + bash sia davvero sostenibile nel medio periodo.

Se gestisci un ambiente Docker di piccola-media scala in produzione e stai usando Watchtower in modalità auto-update di default, fai una cosa subito: domani mattina, prima di qualunque altra attività, passalo in monitor-only e disattiva l'aggiornamento automatico su tutti i container stateful (database, code queue, Redis). È un cambio di configurazione di cinque minuti che ti protegge dall'incidente che prima o poi arriverà. Poi, quando hai un po' di tempo, costruisci lo script di aggiornamento con health check e rollback descritto sopra, calibra il HEALTHCHECK di ogni Dockerfile, e aggiungi un paio di smoke test applicativi specifici per le funzionalità critiche del tuo sistema. Se invece gestisci un'infrastruttura container più complessa e sospetti di aver già superato la soglia di sostenibilità di docker-compose + Watchtower, oppure se stai pianificando una migrazione a un orchestratore ma non sai da dove iniziare, contattami per una consulenza: in una giornata di audit mappo lo stato attuale della tua pipeline, identifico i tre punti di rottura più probabili nei prossimi sei mesi, e ti consegno un piano di rimediazione calibrato sul tuo rapporto fra budget, competenze interne e tolleranza al rischio.

Ultima modifica: