Strategia di aggiornamento Docker: aggiornamenti senza downtime su un VPS
Quattro metodi progressivi per aggiornare i container Docker su un VPS, dal semplice pull-and-replace al deployment blue-green senza downtime con Traefik. Pinning delle immagini, rollback, notifiche Diun e docker-rollout.
Aggiornare i container Docker su un VPS non deve per forza significare downtime. L'approccio giusto dipende da cosa stai eseguendo e quanta interruzione puoi tollerare. Un blog personale regge qualche secondo di downtime durante un docker compose up -d. Un prodotto SaaS con clienti paganti no.
Questa guida copre quattro metodi, dal più semplice al più resiliente. Ognuno si basa sul precedente. Parti da quello che si adatta alla tua situazione e passa al livello successivo quando ne hai bisogno.
Prerequisiti: Un VPS con Debian 12 o Ubuntu 24.04 con Docker Engine 27+ e Docker Compose v2 installati. Tutti i comandi usano la sintassi del plugin docker compose (non il binario deprecato docker-compose v1). Docker in produzione su un VPS: cosa si rompe e come risolvere
Come fissare le immagini Docker a una versione specifica?
Fissa le tue immagini a una versione minor o patch nel tuo file compose. Il tag latest è un bersaglio mobile che può introdurre breaking change senza preavviso. Il pinning ti dà il controllo su quando avvengono gli aggiornamenti e rende possibile il rollback mantenendo l'immagine precedente in locale.
Diverse strategie di tag comportano diversi livelli di rischio:
| Formato tag | Esempio | Livello di rischio | Comportamento aggiornamento |
|---|---|---|---|
latest |
nginx:latest |
Alto | Qualsiasi versione, in qualsiasi momento. Non puoi sapere cosa è cambiato. |
| Solo major | nginx:1 |
Medio-alto | Può saltare da 1.25 a 1.27. Le versioni minor possono cambiare il comportamento. |
| Minor | nginx:1.27 |
Basso | Riceve aggiornamenti patch (1.27.0 a 1.27.3). Sicuro per la maggior parte dei workload. |
| Patch | nginx:1.27.3 |
Molto basso | Versione esatta. Nessun aggiornamento a sorpresa. Aggiorni manualmente. |
| Digest | nginx:1.27.3@sha256:6f12... |
Minimo | Immagine identica byte per byte ogni volta. Immune alla mutazione dei tag. |
Per la maggior parte dei servizi in produzione, fissa alla versione minor (image: postgres:16.6). È il giusto equilibrio tra patch di sicurezza e stabilità. Per i servizi dove la riproducibilità conta (CI, ambienti regolamentati), fissa al digest completo.
services:
app:
image: myapp:2.4.1
# Not: image: myapp:latest
db:
image: postgres:16.6
Registra i digest delle immagini attuali prima di aggiornare. Ti serviranno per un rollback:
docker image inspect --format='{{index .RepoDigests 0}}' $(docker compose images app -q)
myapp@sha256:a1b2c3d4e5f6...
Come configurare gli health check in Docker Compose?
Gli health check dicono a Docker se il tuo container sta effettivamente funzionando, non solo se è in esecuzione. Tutti i pattern di aggiornamento senza downtime dipendono da essi. Senza un health check, Docker non ha modo di sapere se il nuovo container è pronto prima di rimuovere il vecchio.
Aggiungi un blocco healthcheck a ogni servizio nel tuo file compose. Il comando test viene eseguito all'interno del container all'intervallo specificato. Docker segna il container come healthy solo dopo che il test passa.
services:
app:
image: myapp:2.4.1
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s
Cosa fa ogni campo:
- test: Il comando da eseguire.
CMDlo esegue direttamente. UsaCMD-SHELLse hai bisogno di funzionalità shell come le pipe. - interval: Tempo tra i controlli. 15s è ragionevole per i servizi web.
- timeout: Quanto aspettare che il comando finisca prima di considerarlo fallito.
- retries: Numero di fallimenti consecutivi prima che Docker segni il container come
unhealthy. - start_period: Periodo di grazia dopo l'avvio del container. Gli health check durante questa finestra non contano per la soglia di fallimento. Impostalo abbastanza lungo per l'avvio della tua applicazione.
Per i servizi che non hanno curl installato, usa il controllo nativo offerto dal servizio:
db:
image: postgres:16.6
healthcheck:
test: ["CMD-SHELL", "pg_isready -U myuser -d mydb"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
cache:
image: redis:7.4
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
Dopo aver avviato i tuoi servizi, verifica che gli health check stiano passando:
docker compose ps
NAME IMAGE STATUS PORTS
app myapp:2.4.1 Up 2 minutes (healthy) 0.0.0.0:8080->8080/tcp
db postgres:16.6 Up 2 minutes (healthy) 5432/tcp
Lo stato (healthy) significa che il tuo health check è configurato e sta passando. Se vedi (health: starting), il container è ancora nel suo start_period. Se vedi (unhealthy), controlla i log dell'health check:
docker inspect --format='{{json .State.Health}}' $(docker compose ps -q app) | python3 -m json.tool
Come aggiornare un container Docker sul mio VPS?
Esegui docker compose pull per scaricare la nuova immagine, poi docker compose up -d per sostituire il container. Docker Compose ferma il vecchio container, lo rimuove e ne avvia uno nuovo dall'immagine aggiornata. Questo causa una breve interruzione (tipicamente 2-10 secondi) mentre il nuovo container si avvia e supera il suo health check.
Passo dopo passo: l'aggiornamento semplice
Prima di aggiornare, fai il backup dei tuoi volume. Un aggiornamento fallito con dati corrotti è molto peggio di qualche secondo di downtime.
Leggi il changelog della nuova versione. Cerca breaking change, opzioni di configurazione deprecate e passaggi di migrazione necessari. Ci vogliono cinque minuti e ti risparmiano ore di debug.
# Pull the new image
docker compose pull app
# Check what changed
docker compose up -d --dry-run
Il flag --dry-run (Docker Compose v2.20+) mostra cosa farà Compose senza farlo realmente. Vedrai quali container verranno ricreati:
DRY RUN MODE - service "app" - Pull
DRY RUN MODE - Container app-1 - Recreate
DRY RUN MODE - Container app-1 - Started
Applica l'aggiornamento:
docker compose up -d app
[+] Running 1/1
✔ Container app-1 Started 0.8s
Verifica che il nuovo container sia healthy:
docker compose ps app
NAME IMAGE STATUS PORTS
app myapp:2.5.0 Up 15 seconds (healthy) 0.0.0.0:8080->8080/tcp
Poi testa dall'esterno del server per assicurarti che il servizio sia raggiungibile:
curl -s -o /dev/null -w "%{http_code}" https://app.example.com/health
200
Quando aggiornare i tuoi container Docker?
Non tutti gli aggiornamenti hanno la stessa urgenza. Un calendario di aggiornamento uniforme porta a rischi inutili o a patch di sicurezza mancati.
- Patch di sicurezza (CVE): Applica immediatamente. Iscriviti agli avvisi di sicurezza delle tue immagini. Un CVE noto in un container esposto pubblicamente viene sfruttato entro ore dalla divulgazione, non giorni.
- Versioni patch (es.: 2.4.1 a 2.4.2): Pianifica settimanalmente o bisettimanalmente. Sono fix di bug. Leggi il changelog, aggiorna, verifica.
- Versioni minor (es.: 2.4 a 2.5): Pianifica mensilmente. Testa prima in un ambiente di staging se ne hai uno. Controlla il changelog per cambiamenti di comportamento.
- Versioni major (es.: 2.x a 3.x): Pianifica e testa. Le versioni major rompono le cose. Leggi la guida alla migrazione. Testa su un VPS separato o in locale prima di toccare la produzione.
Come fare rollback di un container Docker a un'immagine precedente?
Docker Compose non ha un comando di rollback integrato. Per tornare indietro: modifica il tuo file compose per fissare il tag o digest dell'immagine precedente, poi esegui docker compose up -d. Il container si riavvia con la vecchia immagine. Funziona solo se hai conservato la vecchia immagine in locale (non eseguire docker image prune subito dopo l'aggiornamento).
Rollback passo dopo passo
Supponiamo che tu abbia aggiornato myapp dalla 2.4.1 alla 2.5.0 e la nuova versione sia rotta.
- Verifica che la vecchia immagine sia ancora disponibile in locale:
docker images myapp
REPOSITORY TAG IMAGE ID CREATED SIZE
myapp 2.5.0 abc123def456 2 hours ago 185MB
myapp 2.4.1 789fed654cba 2 weeks ago 182MB
- Modifica il tuo file compose per fissare la versione precedente:
services:
app:
image: myapp:2.4.1
- Esegui il rollback:
docker compose up -d app
[+] Running 1/1
✔ Container app-1 Started 0.7s
- Verifica che il rollback abbia funzionato:
docker compose ps app
NAME IMAGE STATUS PORTS
app myapp:2.4.1 Up 10 seconds (healthy) 0.0.0.0:8080->8080/tcp
Se hai già eliminato la vecchia immagine, Docker la scaricherà di nuovo dal registry (ammesso che il tag esista ancora). Per la massima sicurezza, annota il digest completo (sha256:...) prima di aggiornare. I digest sono immutabili. I tag possono essere sovrascritti.
Automatizza con uno script pre-aggiornamento
Salva lo stato attuale prima di ogni aggiornamento, così il rollback è sempre a un comando di distanza:
#!/bin/bash
# save-state.sh - Run before every update
COMPOSE_FILE="${1:-docker-compose.yml}"
DATE=$(date +%Y%m%d-%H%M%S)
BACKUP_DIR="./rollback/${DATE}"
mkdir -p "${BACKUP_DIR}"
cp "${COMPOSE_FILE}" "${BACKUP_DIR}/"
docker compose ps --format json > "${BACKUP_DIR}/containers.json"
docker compose images --format json > "${BACKUP_DIR}/images.json"
echo "State saved to ${BACKUP_DIR}"
chmod 700 save-state.sh
Watchtower è ancora mantenuto nel 2026?
Watchtower è stato archiviato il 17 dicembre 2025. I maintainer non usano più Docker e hanno fermato lo sviluppo. L'ultima release è la v1.7.1. Ancora più importante, l'SDK Docker di Watchtower usa l'API v1.25, ma Docker Engine 29 ha alzato la versione minima dell'API a v1.44. Watchtower è incompatibile con le versioni attuali di Docker a meno che non si abbassi manualmente il minimo dell'API con DOCKER_MIN_API_VERSION=1.25 nella configurazione del daemon. Quello è un workaround, non una soluzione.
Se usi Watchtower oggi, pianifica la migrazione. Per notifiche automatiche sugli aggiornamenti senza riavvio automatico, usa Diun. Per aggiornamenti automatici senza downtime, usa docker-rollout dietro un reverse proxy.
Come Diun ti notifica degli aggiornamenti delle immagini Docker?
Diun (Docker Image Update Notifier) monitora i tuoi registry Docker e invia notifiche quando sono disponibili nuove versioni delle immagini. Non aggiorna i container. Ti informa che esiste un aggiornamento così puoi leggere il changelog e aggiornare secondo i tuoi tempi. È l'approccio «sapere prima di agire».
Aggiungi Diun al tuo file compose esistente o creane uno dedicato:
services:
diun:
image: crazymax/diun:4
command: serve
volumes:
- "diun-data:/data"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
environment:
TZ: "Europe/Berlin"
DIUN_WATCH_WORKERS: "10"
DIUN_WATCH_SCHEDULE: "0 6 * * *"
DIUN_PROVIDERS_DOCKER: "true"
DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT: "true"
DIUN_NOTIF_SLACK_WEBHOOKURL_FILE: "/run/secrets/slack_webhook"
secrets:
- slack_webhook
restart: unless-stopped
secrets:
slack_webhook:
file: ./secrets/slack_webhook.txt
volumes:
diun-data:
L'URL del webhook Slack va in un file di secrets, non in una variabile d'ambiente, perché Docker secrets lo tiene fuori dall'output di docker inspect e dagli elenchi dei processi. Crea il file di secrets con permessi ristretti:
mkdir -p secrets
echo "https://hooks.slack.com/services/YOUR/WEBHOOK/URL" > secrets/slack_webhook.txt
chmod 600 secrets/slack_webhook.txt
Impostazioni principali:
- DIUN_WATCH_SCHEDULE: Espressione cron.
0 6 * * *controlla ogni giorno alle 06:00. Adatta alla tua finestra di manutenzione. - DIUN_PROVIDERS_DOCKER_WATCHBYDEFAULT: Quando
true, Diun monitora tutti i container in esecuzione. Imposta afalsee usa le label per un monitoraggio selettivo. - Mount del socket Docker: Solo lettura (
:ro) perché Diun legge solo i metadati dei container. Non avvia né ferma mai container.
Per il monitoraggio selettivo (consigliato per stack più grandi), imposta WATCHBYDEFAULT a false e aggiungi label ai container che vuoi monitorare:
services:
app:
image: myapp:2.4.1
labels:
- "diun.enable=true"
- "diun.watch_repo=true"
Avvia Diun e controlla i log:
docker compose up -d diun
docker compose logs diun --tail 20
diun | Thu, 19 Mar 2026 06:00:01 CET INF Starting Diun version=v4.31.0
diun | Thu, 19 Mar 2026 06:00:01 CET INF Configuration loaded from 5 environment variable(s)
diun | Thu, 19 Mar 2026 06:00:02 CET INF Cron triggered
diun | Thu, 19 Mar 2026 06:00:03 CET INF New image found image=docker.io/myapp:2.5.0 provider=docker
Quando Diun trova una nuova immagine, invia un messaggio Slack con il nome dell'immagine, il tag attuale e il nuovo tag. Decidi tu se e quando aggiornare.
Come fa docker-rollout a ottenere aggiornamenti senza downtime?
docker-rollout è un plugin CLI Docker che esegue deployment blue-green per i servizi Compose. Avvia un nuovo container dall'immagine aggiornata, aspetta che l'health check passi, poi rimuove il vecchio container. Il traffico non raggiunge mai un container non funzionante perché il reverse proxy instrada solo verso container healthy.
Requisiti:
- Un reverse proxy (Traefik, Caddy o nginx-proxy) che instrada il traffico al tuo servizio
- Health check definiti nel tuo file compose
- Nessuna direttiva
container_namesul servizio (docker-rollout gestisce i nomi dei container) - Nessun mapping diretto di
portssul servizio (il reverse proxy gestisce l'esposizione delle porte)
Installare docker-rollout
mkdir -p /usr/local/lib/docker/cli-plugins
curl -fsSL https://raw.githubusercontent.com/wowu/docker-rollout/main/docker-rollout \
-o /usr/local/lib/docker/cli-plugins/docker-rollout
chmod +x /usr/local/lib/docker/cli-plugins/docker-rollout
Dovrebbe mostrare la sua versione dopo l'installazione:
docker rollout --version
docker-rollout version v0.13
Esempio: Traefik + docker-rollout
Un file compose minimale per un'app web dietro Traefik con health check. L'app non ha né ports né container_name perché docker-rollout deve gestire lo scaling.
services:
traefik:
image: traefik:3.3
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
ports:
- "80:80"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
restart: unless-stopped
app:
image: myapp:2.4.1
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
- "traefik.http.routers.app.entrypoints=web"
- "traefik.http.services.app.loadbalancer.server.port=8080"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s
restart: unless-stopped
Deploy senza downtime
Scarica la nuova immagine e usa docker rollout invece di docker compose up -d:
docker compose pull app
docker rollout app
==> Scaling 'app' to '2' instances
Container myproject-app-2 Creating
Container myproject-app-2 Created
Container myproject-app-2 Starting
Container myproject-app-2 Started
==> Waiting for new containers to be healthy (timeout: 60 seconds)
==> Stopping and removing old containers
Durante questo processo, Traefik rileva il nuovo container tramite il socket Docker, instrada il traffico verso di esso una volta healthy e smette di instradare verso il vecchio container prima che venga rimosso. I tuoi utenti non vedono alcuna interruzione.
Se il nuovo container fallisce l'health check, docker-rollout annulla e il vecchio container continua a funzionare. Nessun intervento manuale necessario.
Cos'è il deployment blue-green con Docker e Traefik?
Il deployment blue-green esegue due copie del tuo servizio (blue e green). Una serve il traffico live mentre l'altra resta inattiva. Per fare il deploy, aggiorni la copia inattiva, verifichi che funzioni, poi commuti il traffico. Questo ti dà un rollback istantaneo ricommutando alla copia precedente.
Questo è il concetto dietro docker-rollout, ma puoi implementarlo manualmente per avere più controllo. Un esempio minimale con la configurazione dinamica di Traefik:
services:
traefik:
image: traefik:3.3
command:
- "--providers.file.directory=/etc/traefik/dynamic"
- "--providers.file.watch=true"
- "--entrypoints.web.address=:80"
ports:
- "80:80"
volumes:
- "./traefik/dynamic:/etc/traefik/dynamic:ro"
restart: unless-stopped
app-blue:
image: myapp:2.4.1
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s
app-green:
image: myapp:2.4.1
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s
Un file di configurazione dinamica di Traefik controlla quale copia riceve il traffico:
# traefik/dynamic/app.yml
http:
routers:
app:
rule: "Host(`app.example.com`)"
service: app
entryPoints:
- web
services:
app:
loadBalancer:
servers:
- url: "http://app-blue:8080"
Per fare il deploy: aggiorna l'immagine di app-green, avvialo, aspetta che sia healthy, poi modifica app.yml per puntare a app-green. Traefik rileva il cambiamento automaticamente perché watch=true. Per fare rollback, modifica il file per puntare di nuovo a app-blue.
Questo approccio richiede più lavoro di docker-rollout. Usalo quando hai bisogno di controllo esplicito sullo switch, quando vuoi eseguire smoke test sulla nuova versione prima di commutare il traffico, o quando più servizi devono commutare insieme.
Quale metodo di aggiornamento devo usare?
Scegli il metodo che corrisponde alla tua tolleranza al downtime e alla complessità della tua infrastruttura.
| Metodo | Downtime | Complessità | Rollback | Reverse proxy richiesto | Adatto per |
|---|---|---|---|---|---|
docker compose pull + up |
2-10 secondi | Bassa | Manuale (modifica file compose) | No | Progetti personali, tool interni |
| Diun + aggiornamento manuale | Come sopra | Bassa | Come sopra | No | Team che vogliono visibilità prima di aggiornare |
| docker-rollout | Nessuno | Media | Automatico (annulla in caso di errore) | Sì | Servizi di produzione su un singolo VPS |
| Blue-green (manuale) | Nessuno | Alta | Istantaneo (switch del file di config) | Sì | Stack multi-servizio, ambienti regolamentati |
Flusso decisionale:
- Sono accettabili 2-10 secondi di downtime? Usa
docker compose pull && docker compose up -d. - Vuoi sapere degli aggiornamenti prima di applicarli? Aggiungi Diun.
- È richiesto zero downtime? Hai un reverse proxy? Usa docker-rollout.
- Hai bisogno di controllo esplicito sullo switch del traffico? Implementa blue-green manualmente.
Qualcosa non funziona?
Il container si avvia ma mostra (unhealthy)
Controlla il comando dell'health check. Eseguilo manualmente dentro il container:
docker compose exec app curl -f http://localhost:8080/health
Se fallisce, il problema è nella tua applicazione, non in Docker. Controlla i log dell'applicazione:
docker compose logs app --tail 50
La vecchia immagine è stata eliminata, impossibile fare rollback
Se il tag esiste ancora nel registry, docker compose pull la scaricherà. Se hai fissato per digest, Docker scarica l'immagine esatta indipendentemente dai cambiamenti dei tag:
image: myapp:2.4.1@sha256:789fed654cba...
docker-rollout si blocca durante il deployment
L'health check non passa entro il timeout. Controlla l'intervallo e i retry dell'health check. Aumenta il timeout:
docker rollout -t 120 app
Watchtower ha smesso di funzionare dopo l'aggiornamento di Docker
Docker Engine 29 richiede l'API v1.44 come minimo. Watchtower usa l'API v1.25. Migra a Diun per le notifiche o docker-rollout per aggiornamenti automatizzati senza downtime.
Diun non rileva le nuove immagini
Controlla la programmazione cron in DIUN_WATCH_SCHEDULE. Avvia una scansione manuale:
docker compose exec diun diun image list
Controlla i log di Diun per errori di autenticazione con il registry:
docker compose logs diun --tail 30
Copyright 2026 Virtua.Cloud. Tutti i diritti riservati. Questo contenuto è un'opera originale del team Virtua.Cloud. La riproduzione, ripubblicazione o redistribuzione senza autorizzazione scritta è vietata.
Pronto a provare?
Distribuisci il tuo server in pochi secondi. Linux, Windows o FreeBSD.
Vedi piani VPS