Docker Compose: avvio automatico dopo il riavvio
Scopri come riavviare gli stack Docker Compose dopo un reboot: policy restart, perché on-failure non basta e quando conviene usare un'unità systemd.
La risposta breve
I servizi Docker Compose si avviano all'avvio del sistema quando valgono contemporaneamente due condizioni. Il daemon Docker deve essere abilitato come servizio di sistema e ogni servizio nel file deve avere una policy di riavvio unless-stopped o always. Aggiungete restart: unless-stopped a ogni servizio ed eseguite docker compose up -d una volta: dopo un riavvio, i container si riavvieranno automaticamente. Nel caso comune non serve altro.
Serve un'unità systemd solo quando l'ordine di avvio è importante: per uno stack che dipende da un disco montato, da un'interfaccia VPN o da una condivisione di rete che non è pronta quando viene avviato il daemon Docker. Questo caso è reale e viene trattato nella seconda metà di questa guida. Se state ancora imparando a usare le definizioni dei servizi e i volumi, iniziate dalle basi di Docker Compose su un VPS e poi tornate qui.
Impostare la policy di riavvio in compose.yaml
La policy si specifica una volta per ogni servizio. Non esiste un'impostazione globale. Se si dimentica un servizio, questo rimane arrestato dopo il riavvio, mentre il resto dello stack si avvia.
services:
app:
image: nginx:1.27
restart: unless-stopped
ports:
- "8080:80"
db:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_PASSWORD: change-me
volumes:
- dbdata:/var/lib/postgresql/data
volumes:
dbdata:Applicarla, quindi leggere la policy dal container in esecuzione:
docker compose up -d
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app)Il comando restituisce unless-stopped. Se restituisce no, il file è stato modificato, ma il container non è mai stato ricreato.
Questo è l'errore più comune. La policy di riavvio è memorizzata nel container, non nel file YAML. La modifica di compose.yaml non cambia un container già esistente. Anche docker compose restart non risolve il problema, perché arresta e avvia lo stesso oggetto container senza modificarne la configurazione. Solo docker compose up -d confronta il file con i container in esecuzione, rileva la modifica della policy e li ricrea.
Per un container che non si vuole ricreare subito, modificare la policy direttamente:
docker update --restart unless-stopped my-containerModificare comunque anche il file YAML. docker update modifica il container in esecuzione, mentre il successivo docker compose up -d leggerà il file e ripristinerà il valore precedente.
Che cosa fa realmente ogni valore di riavvio
Docker definisce quattro valori. La differenza tra questi valori si manifesta solo quando la macchina viene riavviata o il daemon viene riavviato.
noè il valore predefinito. Il container non viene mai riavviato automaticamente, in nessuna circostanza.alwaysriavvia il container ogni volta che si arresta. Se lo hai arrestato manualmente, viene comunque riavviato al successivo avvio del daemon Docker. Questo comportamento è spesso sorprendente: un container che avevi arrestato deliberatamente la settimana scorsa è nuovamente in esecuzione dopo un riavvio.unless-stoppedsi comporta comealways, con l'eccezione che un container arrestato manualmente rimane arrestato anche dopo il riavvio del daemon. Questo è il valore da usare per un servizio che arresti occasionalmente per eseguire interventi di manutenzione.on-failureriavvia il container solo quando termina con un codice di uscita diverso da zero. Puoi limitare il numero di tentativi, come inrestart: on-failure:3.
Per uno stack che deve essere semplicemente in esecuzione quando il server è attivo, unless-stopped è il valore predefinito corretto. Scegli always solo quando vuoi impedire che un container rimanga arrestato.
Perché il riavvio: on-failure non sopravvive a un riavvio
Molti scelgono on-failure perché sembra una scelta prudente, poi scoprono che ogni container è arrestato dopo il primo riavvio. Il motivo è nella definizione. on-failure reagisce a una sola situazione: il processo del container termina con un codice di errore.
Un riavvio non è un errore. Quando l'host si arresta, systemd arresta docker.service e il daemon arresta deliberatamente ogni container. Il container non ha avuto un errore, quindi la policy non ha nulla a cui reagire. Al riavvio, il daemon controlla i container da riprendere e un container on-failure arrestato correttamente non rientra tra questi. Rimane nello stato exited.
Puoi verificarlo direttamente. Imposta restart: on-failure su un servizio, esegui docker compose up -d, riavvia il sistema, quindi esegui:
docker compose ps -aIl servizio viene elencato con lo stato Exited e uno stato simile a Exited (0) 2 minutes ago. Non c'è alcun problema e non viene registrato alcun errore, rendendo difficile la diagnosi. La policy ha fatto esattamente ciò che indica.
on-failure è comunque utile. È adatto a un container che esegue un job e può terminare in modo anomalo, quando vuoi un numero limitato di tentativi e nessun ciclo di riavvio. È lo strumento sbagliato per mantenere attivo un servizio a esecuzione prolungata dopo i riavvii.
Le policy di riavvio funzionano solo se il servizio Docker viene avviato all'avvio del sistema
Le policy di riavvio vengono applicate dal demone Docker. Se il demone non viene avviato, non viene applicata alcuna policy. Verifica lo stato:
systemctl is-enabled docker
systemctl is-enabled containerdEntrambi i comandi dovrebbero restituire enabled. I pacchetti del repository ufficiale Docker li abilitano durante l'installazione, quindi su un server appena installato questa verifica di solito ha esito positivo. Se uno dei due restituisce disabled, correggi la configurazione:
sudo systemctl enable --now docker containerdÈ importante comprendere un dettaglio. Ubuntu include anche docker.socket, che avvia il demone su richiesta la prima volta che un processo comunica con l'API Docker. Si può vedere docker.socket abilitato, presumere che il demone sia configurato correttamente e disabilitare docker.service per risparmiare memoria. All'avvio del sistema, nessun processo chiama l'API, quindi il socket non viene mai utilizzato, il demone non viene avviato e nessun container viene avviato finché non si esegue il primo comando docker. L'attivazione tramite socket non sostituisce l'abilitazione di docker.service.
Quando un'unit systemd è la soluzione migliore
Le policy di riavvio non gestiscono l'ordine di avvio rispetto al resto del sistema. Il demone si avvia e porta i container allo stato attivo appena possibile. Se lo stack usa un bind mount per una directory presente su un volume separato, una condivisione NFS (network file system) o un disco crittografato, i container possono avviarsi prima che il percorso esista. Docker crea senza problemi una directory vuota nel punto di mount e avvia il container utilizzandola; il database viene quindi avviato senza dati.
Scrivi un'unit systemd quando si verifica una di queste condizioni. Lo stack richiede che prima sia pronto un mount, un'interfaccia VPN o un'altra unit. Vuoi che systemctl stop myapp e systemctl start myapp funzionino come per ogni altro servizio del sistema. Oppure vuoi che lo stack venga arrestato correttamente durante lo spegnimento, invece di essere terminato insieme al demone. Se le unit systemd sono una novità per te, scrivere un servizio e un timer systemd descrive più dettagliatamente il formato del file.
Scrivere l'unità systemd
Inserire lo stack in un percorso fisso, al di fuori di una directory home. /srv/myapp è una scelta appropriata, perché un'unità eseguita prima che qualcuno effettui l'accesso non deve leggere /home.
Creare /etc/systemd/system/myapp.service:
[Unit]
Description=myapp docker compose stack
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
RequiresMountsFor=/srv/myapp/data
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/srv/myapp
ExecStart=/usr/bin/docker compose up -d --remove-orphans
ExecStop=/usr/bin/docker compose down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.targetAbilitare e avviare l'unità:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp.serviceUn'unità funzionante mostra Active: active (exited). La prima volta può sembrare errato. È corretto: Type=oneshot con RemainAfterExit=yes significa che l'unità ha eseguito il comando, il comando è terminato e systemd mantiene l'unità contrassegnata come attiva, in modo che ExecStop venga eseguito durante l'arresto.
Ogni riga ha una funzione precisa. Requires=docker.service indica che l'unità termina rapidamente invece di eseguire docker compose su un socket non disponibile. After= imposta l'ordine, perché Requires= da solo non lo fa. RequiresMountsFor= fa in modo che systemd includa l'unità di mount per quel percorso e attenda che sia pronta. Questo è il motivo principale per usare un'unità invece di una policy di riavvio. TimeoutStartSec=0 impedisce a systemd di terminare il job di avvio mentre è ancora in corso il pull di un'immagine di grandi dimensioni.
Una nota sulla combinazione dei due meccanismi. La documentazione di Docker sconsiglia di combinare le restart policy con un process manager dell'host. L'avvertenza riguarda un process manager che supervisiona direttamente il processo del container e lo riavvia mentre il daemon tenta di fare lo stesso. Un'unità Type=oneshot non supervisiona alcun processo. È quindi corretto mantenere restart: unless-stopped nel file compose insieme a questa unità, ed è la configurazione desiderata. systemd gestisce l'ordine durante l'avvio, mentre il daemon gestisce un container che termina inaspettatamente durante la notte.
Verifica con un riavvio reale
Non esiste un sostituto per il test reale. systemctl restart docker non verifica l'ordine dei mount e docker compose down seguito da docker compose up -d non verifica nulla relativo all'avvio.
sudo rebootAttendi, riconnettiti e verifica in questo ordine:
uptime
systemctl is-active docker
docker compose psuptime conferma che stai esaminando una macchina che è stata realmente riavviata. docker compose ps, eseguito dalla directory dello stack, dovrebbe elencare ogni servizio come running, con un uptime vicino a quello della macchina. Il servizio che mostra Exited è quello da esaminare.
Se un servizio non è stato avviato, il log del demone copre la finestra di avvio:
journalctl -u docker.service -b --no-pager | tail -50Per uno stack gestito da una unit, journalctl -u myapp.service -b --no-pager mostra l'output docker compose esatto dell'avvio, incluso un pull dell'immagine non riuscito o un file .env mancante.
Elementi che interrompono silenziosamente l'avvio automatico
I container creati con docker compose run non ricevono mai la policy di riavvio definita nel file. Compose li tratta come container temporanei. Se un servizio sembra ignorare la propria policy, verifica se è stato avviato con run invece di up.
Un percorso relativo in un volume o in una voce env_file viene risolto rispetto alla directory del file Compose. Funziona dalla shell e funziona da un'unità che imposta WorkingDirectory. Non funziona da un'unità che non lo imposta, perché la directory di lavoro è quindi /.
Docker rootless è un caso distinto. Il daemon viene eseguito come servizio dell'utente e un servizio dell'utente si arresta quando termina l'ultima sessione di quell'utente. Abilitalo per l'utente e consenti che continui a essere eseguito anche senza utenti connessi:
systemctl --user enable docker
sudo loginctl enable-linger $USERSenza enable-linger, il daemon rootless si arresta quando esegui il logout e con esso si arrestano anche i container. Questo produce esattamente l'effetto di una policy di riavvio non funzionante.
Un'ultima considerazione. Gli aggiornamenti automatici di sicurezza possono riavviare un server a un'ora prestabilita. Questo è utile solo se lo stack si avvia nuovamente da solo. Configurare questa funzione su una macchina nuova rientra nelle attività iniziali, insieme a primi dieci minuti su un nuovo VPS.
FAQ
Qual è la differenza tra restart: always e restart: unless-stopped?
Entrambe riavviano il container quando si arresta autonomamente. La differenza si presenta dopo l'arresto manuale del container. Con always, il container viene avviato di nuovo al successivo avvio del demone Docker, quindi un riavvio annulla l'arresto manuale. Con unless-stopped, il demone ricorda che il container è stato arrestato intenzionalmente e lo lascia fermo. Usa unless-stopped, a meno che tu non voglia specificamente che un container rimanga arrestato.
Ho aggiunto restart: unless-stopped, ma il container non viene ancora avviato dopo un riavvio. Perché?
La policy è memorizzata nel container, non nel file, e la modifica del file YAML non aggiorna un container già esistente. Esegui docker compose up -d per fare in modo che Compose lo ricrei, quindi verifica con docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app). Se restituisce no, il container è stato creato prima della modifica. L'altra causa comune è che docker.service non sia abilitato; puoi verificarlo con systemctl is-enabled docker.
Mi serve un'unità systemd se uso già le policy di riavvio?
Di solito no. Una policy di riavvio è sufficiente per uno stack che dipende solo dalla rete, come avviene per la maggior parte degli stack. Aggiungi un'unità quando i container dipendono da una risorsa che non è pronta all'avvio del demone Docker, ad esempio un disco esterno, un volume crittografato, una condivisione NFS o un'interfaccia VPN. L'unità consente di definire l'ordine tramite After= e RequiresMountsFor=, cosa che una policy di riavvio non può esprimere.
Come posso arrestare definitivamente uno stack senza che venga riavviato al successivo riavvio del sistema?
Con unless-stopped, docker compose stop è sufficiente, perché un container arrestato manualmente non viene ripreso quando il demone viene riavviato. Con always, l'arresto non è sufficiente e il container viene riavviato dopo un reboot. Esegui docker compose down, che rimuove i container, oppure modifica prima la policy con docker update --restart no my-container. Se un'unità systemd gestisce lo stack, esegui anche sudo systemctl disable myapp.service, altrimenti l'unità lo riavvierà.