SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor

Docker Compose: avvio automatico al riavvio

Scopri come riavviare i servizi Docker Compose dopo un reboot con restart policy, perché on-failure non basta e quando serve un'unita systemd.

La risposta breve

I servizi Docker Compose vengono avviati al boot quando sono soddisfatte contemporaneamente due condizioni. Il demone Docker deve essere abilitato come servizio di sistema e ogni servizio definito nel file deve avere una policy di riavvio pari a unless-stopped o always. Aggiungete restart: unless-stopped a ogni servizio ed eseguite una volta docker compose up -d: dopo un riavvio, i container si avvieranno automaticamente. Nel caso comune non è necessario altro.

Serve un'unità systemd solo quando l'ordine di avvio è importante: ad esempio, per uno stack che dipende da un disco montato, da un'interfaccia VPN o da una condivisione di rete che non è ancora pronta quando si avvia il demone Docker. Questo caso è reale e viene trattato nella seconda parte di questa guida. Se state ancora acquisendo familiarità con le definizioni dei servizi e i volumi, iniziate dalle nozioni di base su Docker Compose su un VPS e poi tornate qui.

Impostare la policy di riavvio in compose.yaml

La policy è una riga per ogni servizio. Non esiste un'impostazione globale: se si dimentica un servizio, questo resterà arrestato dopo il riavvio, mentre il resto dello stack verrà avviato.

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:

Applicare la modifica e 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. Modificare compose.yaml non cambia nulla in un container già esistente. Anche docker compose restart non è sufficiente, perché arresta e riavvia lo stesso oggetto container senza modificarne la configurazione. Solo docker compose up -d confronta il file con i container in esecuzione, rileva la modifica alla policy e li ricrea.

Per un container che non si vuole ricreare subito, modificare la policy direttamente:

docker update --restart unless-stopped my-container

Modificare 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.

Cosa comporta realmente ciascun valore di riavvio

Docker definisce quattro valori. La differenza tra loro 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.
  • always riavvia il container ogni volta che si arresta. Se lo si arresta manualmente, viene comunque riavviato al successivo avvio del daemon Docker. Questo comportamento è spesso sorprendente: un container arrestato intenzionalmente la settimana scorsa è nuovamente in esecuzione dopo un riavvio.
  • unless-stopped si comporta come always, con la differenza che un container arrestato manualmente rimane arrestato anche dopo il riavvio del daemon. È il valore da usare per un servizio che viene arrestato occasionalmente per la manutenzione.
  • on-failure riavvia il container solo quando termina con un codice di uscita diverso da zero. È possibile limitare il numero di tentativi, come in restart: on-failure:3.

Per uno stack che deve essere semplicemente in esecuzione quando il server è attivo, unless-stopped è il valore predefinito corretto. Scegliere always solo quando si vuole che un container non rimanga arrestato.

Perché restart: on-failure non sopravvive a un riavvio

Molti scelgono on-failure perché sembra un'impostazione prudente, poi scoprono che ogni container è fermo dopo il primo riavvio. Il motivo è nella definizione. on-failure reagisce a una sola condizione: la terminazione del processo del container con un codice di errore.

Un riavvio non è un errore. Quando l'host si arresta, systemd ferma docker.service, che a sua volta arresta deliberatamente ogni container. Il container non è terminato in modo anomalo, quindi la policy non ha nulla a cui reagire. Durante il riavvio, il daemon verifica quali container deve riprendere, ma 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 -a

Il servizio viene elencato con stato Exited e uno stato simile a Exited (0) 2 minutes ago. Non c'è alcun problema e nei log non compare alcun errore, per questo il problema è difficile da diagnosticare. La policy ha eseguito esattamente quanto previsto.

on-failure resta utile. È adatto a un container che esegue un job e può terminare in modo anomalo. Da solo, restart: on-failure non imposta alcun limite e continua a riavviare un job che continua a fallire. Aggiungi quindi un limite come restart: on-failure:3 quando vuoi un numero massimo di tentativi ed evitare un ciclo di riavvii. È 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 al boot

Le policy di riavvio sono applicate dal demone Docker. Se il demone non viene avviato, non c'è alcun componente che le applichi. Verificate lo stato:

systemctl is-enabled docker
systemctl is-enabled containerd

Entrambi i comandi devono restituire enabled. I pacchetti del repository ufficiale Docker li abilitano durante l'installazione, quindi su un server appena installato questo controllo ha normalmente esito positivo. Se uno dei due restituisce disabled, correggete la configurazione:

sudo systemctl enable --now docker containerd

Esiste un dettaglio importante da comprendere. Ubuntu include anche docker.socket, che avvia il demone su richiesta quando qualcosa comunica per la prima volta con l'API Docker. Si vede docker.socket abilitato, si presume che il demone sia quindi gestito e si disabilita docker.service per risparmiare memoria. Al boot 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 è preferibile un'unità systemd

Le policy di riavvio non gestiscono l'ordine di avvio rispetto al resto del sistema. Il demone si avvia e porta online i container appena possibile. Se lo stack monta una directory da un volume separato, da una condivisione NFS (network file system) o da un disco cifrato, i container possono avviarsi prima che il percorso sia disponibile. Docker crea senza problemi una directory vuota nel punto di montaggio e avvia il container utilizzandola. Il database si avvia quindi senza dati.

Scrivete 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à. Volete che systemctl stop myapp e systemctl start myapp funzionino come per ogni altro servizio del server. Oppure volete arrestare lo stack correttamente durante lo spegnimento, invece di lasciarlo terminare insieme al demone. Se le unità systemd sono una novità, creare un servizio e un timer systemd descrive più dettagliatamente il formato del file.

Scrivere l'unit systemd

Inserisci lo stack in un percorso fisso al di fuori di una directory home. /srv/myapp è una buona scelta, perché un'unit che viene eseguita prima dell'accesso di un utente non deve leggere /home.

Crea /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.target

Abilitala e avviala:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp.service

Un'unit in buono stato 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 nello stato active in modo che ExecStop venga eseguito durante l'arresto.

Ogni riga ha una funzione precisa. Requires=docker.service fa fallire rapidamente l'unit invece di eseguire docker compose su un socket non disponibile. After= definisce l'ordine, cosa che Requires= da solo non fa. RequiresMountsFor= fa caricare a systemd l'unit di mount per quel percorso e attende che sia pronta. Questo è il motivo principale per usare un'unit invece di una restart policy. TimeoutStartSec=0 impedisce a systemd di terminare il job di avvio mentre un'immagine di grandi dimensioni è ancora in fase di download.

Una nota sulla combinazione dei due meccanismi. La documentazione di Docker sconsiglia di combinare le restart policy con un process manager a livello host, indicando che possono verificarsi conflitti. Il conflitto si verifica quando un process manager supervisiona direttamente il processo del container e lo riavvia mentre il daemon tenta di fare la stessa cosa. Un'unit Type=oneshot non supervisiona alcun processo, quindi è corretto mantenere restart: unless-stopped nel file Compose insieme a questa unit. È esattamente ciò che serve. systemd gestisce l'ordine durante il boot, mentre il daemon gestisce un container che termina in modo anomalo alle tre del mattino.

L'unit ha un aspetto diverso quando ciò che vuoi mantenere attivo è un normale processo a esecuzione prolungata invece di uno stack, perché in quel caso non c'è un daemon sottostante e il Restart= di systemd deve occuparsi della supervisione; eseguire dsh senza interfaccia grafica tramite systemd è un esempio completo di questa configurazione, con tanto di utente dedicato e journal.

Verifica con un riavvio reale

Non esiste un sostituto per il test reale. systemctl restart docker non verifica l’ordine di montaggio e docker compose down seguito da docker compose up -d non verifica nulla relativo all’avvio.

sudo reboot

Attendi, riconnettiti e controlla in questo ordine:

uptime
systemctl is-active docker
docker compose ps

uptime conferma che stai verificando 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 daemon copre la finestra di avvio:

journalctl -u docker.service -b --no-pager | tail -50

Per uno stack gestito da una unit, journalctl -u myapp.service -b --no-pager mostra l’output docker compose completo dell’avvio, incluso il pull fallito di un’immagine o un file .env mancante. Il riavvio pianificato è quello che devi monitorare, quindi lascia che la unit ti segnali quelli che non puoi osservare direttamente: una riga OnFailure= indirizzata a un server ntfy self-hosted trasforma uno stack che non è tornato operativo in una notifica push, invece di qualcosa che scopriresti dopo diversi giorni.

Elementi che interrompono l'avvio automatico senza messaggi evidenti

I container creati con docker compose run non ricevono mai la policy di riavvio definita nel file. Compose li considera 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 anche da un'unità che imposta WorkingDirectory. Fallisce da un'unità che non lo imposta, perché in quel caso la directory di lavoro è /.

Docker rootless è un caso distinto. Il daemon viene eseguito come servizio utente e un servizio utente si arresta quando termina l'ultima sessione di quell'utente. Abilitalo per l'utente e consenti che continui a funzionare senza utenti connessi:

systemctl --user enable docker
sudo loginctl enable-linger $USER

Senza enable-linger, il daemon rootless si arresta quando esegui il logout e con esso si arrestano i container. Il comportamento appare identico a quello di una policy di riavvio non funzionante.

Un'ultima considerazione. Gli aggiornamenti di sicurezza automatici possono riavviare un server a un'ora prestabilita. Questo è utile solo se lo stack si avvia nuovamente in autonomia. La configurazione su una macchina nuova rientra nelle attività della prima ora descritte in i primi dieci minuti su un nuovo VPS.

FAQ

Qual è la differenza tra restart: always e restart: unless-stopped?

Entrambi riavviano il container quando si arresta autonomamente. La differenza si manifesta dopo l'arresto manuale del container. Con always, il container viene avviato nuovamente al successivo avvio del daemon Docker, quindi un riavvio annulla l'arresto manuale. Con unless-stopped, il daemon conserva l'informazione che il container è stato arrestato intenzionalmente e lo lascia fermo. Usare unless-stopped, a meno che non si desideri specificamente che un container resti fermo.

Ho aggiunto restart: unless-stopped, ma il container non si avvia dopo un riavvio. Perché?

La policy è memorizzata nel container, non nel file, e la modifica del file YAML non aggiorna un container già esistente. Eseguire docker compose up -d affinché Compose lo ricrei, quindi verificare con docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' $(docker compose ps -q app). Se il comando restituisce no, il container è stato creato prima della modifica. L'altra causa comune è che docker.service non sia abilitato; è possibile verificarlo con systemctl is-enabled docker.

Devo creare un'unità systemd se uso già le restart policy?

Di solito no. Una restart policy è sufficiente per uno stack che richiede soltanto la rete, come avviene per la maggior parte degli stack. Aggiungere un'unità quando i container dipendono da una risorsa che non è pronta all'avvio del daemon Docker, ad esempio un disco esterno, un volume cifrato, una condivisione NFS o un'interfaccia VPN. L'unità consente di definire l'ordine tramite After= e RequiresMountsFor=, cosa che una restart policy non può esprimere.

Come posso arrestare definitivamente uno stack senza che torni attivo al riavvio successivo?

Con unless-stopped è sufficiente docker compose stop, perché un container arrestato manualmente non viene riavviato quando il daemon riparte. Con always, l'arresto non è sufficiente e il container torna attivo dopo un riavvio. Eseguire docker compose down, che rimuove i container, oppure modificare prima la policy con docker update --restart no my-container. Se un'unità systemd gestisce lo stack, eseguire anche sudo systemctl disable myapp.service, altrimenti l'unità lo avvierà nuovamente.