Come impostare un Auto-Restart per i Container Docker con lo stato Unhealthy

In questo articolo vedremo come impostare un sistema di Auto-Restart per i container Docker che, per un qualunque motivo, passano allo stato unhealthy. Tutto questo è possibile grazie al container willfarrell/autoheal.

Se un container diventa unhealthy vorremmo naturalmente riavviarlo per renderlo nuovamente funzionante, e l’ideale sarebbe che ciò avvenisse in automatico. Purtroppo Docker non offre ancora questa funzionalità: è stata proposta, ma non è mai stata implementata.

Per fortuna esiste il container willfarrell/autoheal, che controlla regolarmente lo stato degli altri container e riavvia quelli non sani.

Cosa significa “unhealthy” e perché la restart policy non basta

Molte immagini Docker includono un healthcheck: un comando che Docker esegue a intervalli regolari all’interno del container per verificare che l’applicazione funzioni davvero (ad esempio che il server web risponda). In base all’esito del comando, il container può trovarsi in uno di questi stati:

  • starting: il container è appena partito e l’healthcheck non ha ancora un risultato;
  • healthy: l’healthcheck ha successo;
  • unhealthy: l’healthcheck è fallito per il numero di volte consecutive impostato (retries).

La restart policy di Docker (restart: always o unless-stopped) riavvia un container solo quando il suo processo principale termina. Un container unhealthy, invece, ha il processo ancora in esecuzione (magari bloccato o non più in grado di rispondere), quindi Docker lo lascia com’è. È qui che entra in gioco autoheal.

Possiamo controllare lo stato di salute dei container con il comando:

$ docker ps --format "table {{.Names}}\t{{.Status}}"

che mostrerà, ad esempio, Up 2 hours (healthy) oppure Up 5 minutes (unhealthy). Per vedere solo i container non sani:

$ docker ps --filter health=unhealthy

Importante: autoheal agisce solo sui container che hanno un healthcheck. Un container senza healthcheck non sarà mai unhealthy e quindi non verrà mai riavviato. Nella sezione “Test” qui sotto vedremo come aggiungerne uno nel docker-compose.yml.

Vi sono due modalità di monitoraggio:

  • monitorare tutti i container e riavviare quelli in stato unhealthy;
  • monitorare e riavviare solo un set specifico di container.

Qui di seguito vedremo entrambi i metodi.

Una parola sulla sicurezza

L’immagine willfarrell/autoheal richiede che il container abbia accesso al socket Docker dell’host: /var/run/docker.sock.

Eseguire il mount di /var/run/docker.sock all’interno di un container conferisce di fatto al container, e a tutto ciò che viene eseguito al suo interno, i privilegi di root sull’host sottostante, poiché diventa possibile fare tutto ciò che può fare un utente root o un membro del gruppo docker.

Ad esempio, si potrebbe creare un container, montare la cartella /etc dell’host, modificarne le configurazioni e aprire un vettore di attacco per prendere il controllo dell’host.

L’immagine è in circolazione dal 2017, è molto diffusa, il codice sorgente è disponibile per l’esame pubblico e molte persone la utilizzano (me compreso), quindi il rischio di un comportamento malevolo da parte dell’autore è, a mio parere, piuttosto basso. Tuttavia, se decidiamo di dare a questo container l’accesso a /var/run/docker.sock dell’host, lo facciamo a nostro rischio e pericolo.

Monitorare e riavviare tutti i container

Creiamo una cartella dedicata e al suo interno il file docker-compose.yml:

$ mkdir ~/autoheal
$ cd ~/autoheal
$ nano docker-compose.yml

e incolliamo quanto segue:

services:
  autoheal:
    image: willfarrell/autoheal:latest
    tty: true
    container_name: autoheal
    restart: always
    environment:
      - AUTOHEAL_CONTAINER_LABEL=all
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

Se invece abbiamo già un file docker-compose.yml con altri servizi, possiamo aggiungere il blocco autoheal: alla fine della sezione services: dello stesso file.

Per spiegare cosa succede in questa configurazione:

  • stiamo creando un container chiamato autoheal, dall’immagine willfarrell/autoheal:latest;
  • restart: always fa sì che autoheal stesso riparta in caso di problemi e ad ogni riavvio dell’host;
  • AUTOHEAL_CONTAINER_LABEL=all dice ad autoheal di monitorare tutti i container;
  • montiamo il socket Docker /var/run/docker.sock, in modo che autoheal possa controllare Docker (e riavviare i container).

Una volta salvato il file (CTRL+X, Y e INVIO), eseguiamo nella stessa cartella il comando:

$ docker compose up -d

per scaricare l’immagine e avviare il container autoheal. Il container non registra alcun output finché tutto va bene, quindi possiamo verificare che sia in esecuzione con il comando docker ps, cercando il container autoheal: dovrebbe avere uno stato Up 20 seconds (healthy) o simile.

Quando riavvia un container non sano, autoheal lo scrive nel proprio log (consultabile con docker logs autoheal), ad esempio:

15-05-2023 19:23:17 Container /adsbx (51bc3e3511f5) found to be unhealthy - Restarting container now with 10s timeout

Monitorare e riavviare solo un set specifico di container

Per dire ad autoheal quali container monitorare, dobbiamo applicare loro un’etichetta (label). Il container autoheal, in questo caso, va configurato senza la variabile AUTOHEAL_CONTAINER_LABEL=all: per impostazione predefinita monitora solo i container con la label autoheal=true.

Etichettatura dei container esistenti

Per far monitorare un container da autoheal, aggiungiamo la seguente direttiva alla definizione del servizio nel suo file docker-compose.yml:

labels:
  - autoheal=true

Il servizio aggiornato può quindi apparire come segue (notiamo l’aggiunta della sezione labels:):

services:
  web:
    image: nginx:alpine
    container_name: web
    restart: always
    labels:
      - autoheal=true
    ports:
      - 8080:80
    tmpfs:
      - /run/test
    healthcheck:
      test: ["CMD-SHELL", "wget -q --spider http://localhost/ && test ! -f /run/test/fail"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 20s

Ripetiamo l’operazione per tutti i container che vogliamo far monitorare ad autoheal, quindi applichiamo le modifiche, nella cartella di ciascuna applicazione, con:

$ docker compose up -d

Docker Compose si accorgerà della modifica e ricreerà i container a cui è stata aggiunta la label, mantenendo invariati gli altri. Bello, no?

Distribuire il container autoheal

Come nel caso precedente, creiamo il file ~/autoheal/docker-compose.yml (oppure aggiungiamo il servizio a un file esistente), questa volta senza la variabile d’ambiente:

services:
  autoheal:
    image: willfarrell/autoheal:latest
    tty: true
    container_name: autoheal
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

Avviamo il container con docker compose up -d e verifichiamo con docker ps che sia in esecuzione, esattamente come visto nel metodo precedente.

Se preferiamo usare un nome di label diverso da autoheal, possiamo impostarlo con la variabile AUTOHEAL_CONTAINER_LABEL (ad esempio AUTOHEAL_CONTAINER_LABEL=riavvia): i container dovranno allora avere la label riavvia=true.

Installazione con un solo comando (docker run)

Se non utilizziamo Docker Compose, possiamo avviare autoheal anche direttamente da riga di comando. Per monitorare tutti i container:

$ docker run -d \
    --name autoheal \
    --restart=always \
    -e AUTOHEAL_CONTAINER_LABEL=all \
    -v /var/run/docker.sock:/var/run/docker.sock \
    willfarrell/autoheal

Per monitorare solo i container con la label autoheal=true basta omettere la riga -e AUTOHEAL_CONTAINER_LABEL=all.

Variabili di configurazione

Il comportamento di autoheal si può personalizzare con le seguenti variabili d’ambiente:

VariabilePredefinitoDescrizione
AUTOHEAL_CONTAINER_LABELautohealNome della label usata per selezionare i container da monitorare. Con il valore all vengono monitorati tutti i container.
AUTOHEAL_INTERVAL5Ogni quanti secondi autoheal controlla lo stato dei container.
AUTOHEAL_START_PERIOD0Secondi di attesa all’avvio di autoheal prima di iniziare i controlli.
AUTOHEAL_DEFAULT_STOP_TIMEOUT10Secondi concessi al container per fermarsi prima che venga terminato forzatamente durante il riavvio. Si può impostare per singolo container con la label autoheal.stop.timeout.
CURL_TIMEOUT30Tempo massimo (in secondi) per le chiamate alle API di Docker.
WEBHOOK_URLvuotoSe impostato, autoheal invia una notifica a questo indirizzo ogni volta che riavvia un container.

Ad esempio, per controllare i container ogni 30 secondi e concedere 20 secondi per l’arresto:

    environment:
      - AUTOHEAL_CONTAINER_LABEL=all
      - AUTOHEAL_INTERVAL=30
      - AUTOHEAL_DEFAULT_STOP_TIMEOUT=20

Test: verificare che l’auto-restart funzioni

Prima di fidarci del sistema, è bene verificare che funzioni davvero. Usiamo il container web dell’esempio precedente (con la label autoheal=true). Il suo healthcheck non si limita a controllare che il server web risponda: verifica anche che nella cartella /run/test non esista un file chiamato fail. Questa cartella è montata come tmpfs (cioè in memoria), quindi viene svuotata ad ogni riavvio del container. Ci permette di “rompere” il container a comando, senza toccarne i file.

Creiamo il file fail all’interno del container:

$ docker exec web touch /run/test/fail

Dopo alcuni cicli di controllo (nel nostro esempio, 3 fallimenti consecutivi a distanza di 30 secondi, quindi circa un minuto e mezzo) il container diventerà unhealthy. Possiamo seguire l’evoluzione con:

$ watch -n 5 'docker ps --format "table {{.Names}}\t{{.Status}}"'

Appena il container passa a unhealthy, autoheal lo riavvia: guardando il log con docker logs autoheal troveremo un messaggio simile a quello visto in precedenza. Poiché la cartella /run/test è in memoria, il file fail sparisce con il riavvio, e dopo il start_period il container torna healthy.

Nota: il riavvio fatto da autoheal equivale a un docker restart, che non ricrea il container. Le modifiche scritte nel file system del container (ad esempio un file cancellato) restano quindi anche dopo il riavvio, e se il problema dipende da queste modifiche il container continuerà a diventare unhealthy e verrà riavviato ripetutamente. In quel caso bisogna ricrearlo con docker compose up -d --force-recreate.

Problemi comuni

  • Il container non viene mai riavviato: verifichiamo che abbia un healthcheck (docker inspect --format '{{json .State.Health}}' nome_container deve restituire un risultato diverso da null) e che sia monitorato, cioè che abbia la label autoheal=true oppure che autoheal sia configurato con AUTOHEAL_CONTAINER_LABEL=all.
  • Il container viene riavviato in continuazione: l’healthcheck è probabilmente troppo severo o l’applicazione impiega più tempo del previsto ad avviarsi. Aumentiamo start_period, interval o retries nell’healthcheck del servizio.
  • Errori di accesso al socket Docker: controlliamo che il volume /var/run/docker.sock:/var/run/docker.sock sia presente e che il percorso del socket sull’host sia corretto.

Conclusione

Con un piccolo container in più abbiamo reso i nostri servizi Docker in grado di “curarsi da soli”: quando un’applicazione smette di rispondere, autoheal la riavvia senza il nostro intervento. Consiglio di abbinare questo sistema a un buon backup dei volumi Docker, per essere al sicuro anche nei casi più gravi.

Se avete problemi con la configurazione, lasciate pure un commento qui sotto.

Buon divertimento!

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Torna in alto