tva
← Approfondimenti

Oltre cento container Docker: la nostra routine di controllo mensile

Eseguire più di cento container Docker in produzione non è insolito se i servizi si sono accumulati gradualmente nel corso degli anni. Uno stack Supabase gestito sulla propria infrastruttura conta da solo tredici container. Aggiungendo un frontend web, diversi servizi API, processi in background, uno stack di monitoraggio e l’aggregazione dei log, il numero cresce più rapidamente di quanto previsto dalla maggior parte dei piani. La difficoltà non è distribuire questi container, ma mantenerli.

La maggior parte della documentazione spiega come avviare i container. Ben poca tratta ciò che accade sei mesi dopo, quando il disco si riempie di domenica, quando scopri che un terzo dei container non ha una politica di riavvio o quando un certificato SSL è scaduto senza che nessuno se ne accorgesse, perché l’avviso di monitoraggio era stato silenziato. Questo articolo documenta la nostra routine mensile per un server di produzione con oltre cento container.

Partire dal disco

Il disco è la causa più comune di guasti improvvisi negli ambienti Docker in funzione da tempo. Docker accumula dati in modi che non sono visibili agli strumenti di sistema standard. Eseguire df -h mostra l’utilizzo del filesystem, ma non indica che Docker conserva cinquanta gigabyte di layer di container arrestati, immagini senza riferimenti e cache di compilazione accumulati in sei mesi di distribuzioni iterative.

Il punto di partenza corretto è docker system df, che suddivide l’utilizzo del disco fra immagini, container, volumi locali e cache di compilazione. Il risultato è spesso sorprendente. Abbiamo visto server in cui la sola cache di compilazione superava tutti i layer dei container in esecuzione messi insieme, accumulandosi silenziosamente per mesi di compilazioni avviate dalla CI e mai ripulite.

Il valore che conta di più, però, è la colonna dello spazio recuperabile. Prima di intervenire, fissiamo un riferimento iniziale: quanto spazio è attualmente recuperabile e qual è l’andamento mese su mese. Se lo spazio recuperabile cresce, la pulizia deve diventare più incisiva o più frequente.

La nostra pulizia segue questo ordine. Prima i volumi anonimi a cui non fa più riferimento alcun container. Da Docker 23.0, docker volume prune lascia intatti i volumi con nome inutilizzati, a meno di aggiungere --all; prima di aggiungere questa opzione, verifica che nessun volume scollegato contenga dati ancora necessari:

docker volume prune -f

Poi le immagini senza riferimenti: layer privi di tag e non utilizzati da alcun container, in esecuzione o arrestato:

docker image prune -f

Infine, se abbiamo confermato che una ricostruzione completa è fattibile entro la finestra di ripristino, tutte le immagini inutilizzate più vecchie di sette giorni:

docker image prune -a --filter "until=168h" -f

Il flag -a rimuove tutte le immagini inutilizzate, non solo quelle senza riferimenti. Lo usiamo soltanto dopo aver verificato che tutti i servizi possano essere ricostruiti a partire dal registry entro il tempo di ripristino accettabile. La verifica avviene prima del comando, non dopo.

Politiche di riavvio

Le politiche di riavvio determinano cosa accade quando un container termina inaspettatamente o quando il demone Docker riparte dopo un riavvio dell’host. La maggior parte delle guide alla distribuzione ne parla brevemente. In realtà, una politica di riavvio impostata male può lasciare servizi fermi per due settimane senza che nessuno lo noti, perché l’avviso di monitoraggio puntava all’endpoint sbagliato.

Docker offre quattro politiche di riavvio. no è quella predefinita: il container non si riavvia in nessuna circostanza. always riavvia il container ogni volta che si arresta, indipendentemente dal codice di uscita; dopo un docker stop manuale, riparte solo quando il demone viene riavviato o il container viene avviato di nuovo. unless-stopped si comporta come always, ma rispetta gli arresti espliciti: se esegui docker stop prima di un riavvio, il container resta fermo anche dopo. on-failure[:max-retries] riavvia soltanto in presenza di un codice di uscita diverso da zero, con un eventuale limite ai tentativi prima di rinunciare.

Per servizi web senza stato e processi API usiamo unless-stopped. Se arrestiamo deliberatamente un container durante una finestra di manutenzione, deve restare fermo dopo il riavvio successivo anziché ripartire inaspettatamente. always lo farebbe ripartire al successivo riavvio del demone, anche se lo avessimo arrestato deliberatamente.

Per container di migrazione dei database o attività di inizializzazione eseguite una sola volta, la politica corretta è no. Una migrazione fallita non deve entrare in un ciclo. on-failure:3 è adatto ai container che devono riprovare brevemente ad accedere a una dipendenza temporaneamente indisponibile, per esempio un consumer di una coda esterna in attesa che il broker torni raggiungibile, ma non devono continuare all’infinito.

Il nostro controllo mensile esegue un unico comando su tutti i container:

docker inspect --format '{{.Name}} {{.HostConfig.RestartPolicy.Name}}' $(docker ps -aq)

Ogni container con politica no che non sia intenzionalmente un’attività da eseguire una sola volta viene esaminato. Nella maggior parte dei casi, il servizio è stato avviato con docker run durante un incidente e non è mai stato aggiunto formalmente alla configurazione Compose con una politica di riavvio adeguata.

Rotazione dei log

Il driver di logging predefinito di Docker è json-file. Per impostazione predefinita non impone limiti di dimensione. Un container che produce un flusso modesto di righe di log può generare centinaia di gigabyte in pochi mesi. Non è un problema teorico: è una delle cause più comuni di esaurimento dello spazio nei server di produzione configurati senza una gestione esplicita dei log.

La soluzione è una politica globale in /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m",
    "max-file": "5"
  }
}

Questo limita i log di ciascun container a cinque file da cento megabyte, per un massimo di cinquecento megabyte per container. Dopo la modifica occorre riavviare il demone Docker e, soprattutto, ricreare i container, non semplicemente riavviarli, perché le nuove impostazioni abbiano effetto.

In realtà, l’impostazione daemon.json si applica soltanto ai container creati dopo la modifica. Quelli esistenti conservano indefinitamente la configurazione originale dei log. È l’errore che incontriamo più spesso: si imposta la politica, si riavvia il demone e si presume che tutti i container siano ora conformi. Non lo sono. Il nostro controllo mensile verifica la configurazione dei log di ogni container:

docker inspect --format '{{.Name}} {{.HostConfig.LogConfig}}' $(docker ps -q)

I container senza limiti espliciti di dimensione vengono ricreati con la configurazione aggiornata nella finestra di manutenzione successiva. L’ordine conta: i servizi con stato devono conservare i volumi dati e i servizi dipendenti devono avviarsi nella sequenza corretta.

Scadenza dei certificati SSL

I certificati SSL scadono. Il monitoraggio automatizzato rileva la maggior parte dei casi, ma può anche essere configurato male, generare assuefazione agli avvisi o smettere di funzionare silenziosamente insieme al servizio che dovrebbe sorvegliare. La nostra routine mensile include una verifica manuale indipendente da qualsiasi sistema automatizzato.

Per ogni dominio esposto al pubblico controlliamo direttamente il certificato:

echo | openssl s_client -connect domain.com:443 -servername domain.com 2>/dev/null   | openssl x509 -noout -enddate

Il comando restituisce la data notAfter. Qualsiasi certificato in scadenza entro trenta giorni entra subito nella coda dei rinnovi, indipendentemente da ciò che indica la dashboard di monitoraggio. Il controllo manuale è la verifica di riserva.

Per l’infrastruttura di certificati che gestiamo direttamente per diversi servizi interni, verifichiamo i certificati intermedi separatamente da quelli finali. Un certificato intermedio scaduto fa fallire la validazione dell’intera catena anche se quello finale è ancora valido. Questo guasto è meno evidente della scadenza di un certificato finale: browser e client possono mostrare errori poco chiari anziché il messaggio «certificato scaduto» che la maggior parte dei tecnici si aspetta.

Manteniamo uno script shell che scorre un elenco di domini, estrae la data di scadenza tramite openssl e stampa un avviso per i certificati in scadenza entro trenta giorni e un allarme critico per quelli in scadenza entro sette. Lo script viene eseguito come attività cron, ma lo avviamo anche manualmente durante la revisione mensile per confermare che i risultati di cron siano corretti. Le attività cron falliscono silenziosamente più spesso di quanto ci si aspetti.

Limiti delle risorse dei container

Senza limiti di memoria, un container che si comporta male può esaurire la RAM dell’host e indurre l’OOM killer del kernel a terminare processi non correlati. Senza limiti CPU, un processo fuori controllo può sottrarre risorse ai container vicini abbastanza a lungo da provocare guasti a cascata. Nessuno dei due è un caso limite raro.

La revisione mensile controlla i limiti delle risorse di tutti i container in esecuzione:

docker stats --no-stream --format "table {{.Name}}	{{.CPUPerc}}	{{.MemUsage}}"

La colonna della memoria mostra insieme utilizzo e limite; un container senza limite di memoria mostra come limite la memoria totale dell’host, mentre 0B / 0B compare solo per i container non in esecuzione. Li esaminiamo uno per uno e definiamo un limite adeguato. Per i servizi HTTP senza stato, un limite di memoria pari a due-quattro volte la memoria effettivamente utilizzata è un punto di partenza ragionevole. L’obiettivo non è la precisione assoluta, ma impedire che una crescita senza limiti interrompa gli altri servizi sullo stesso host.

Durante il controllo delle statistiche osserviamo anche la percentuale di CPU. Un container costantemente vicino al cento per cento di CPU (in docker stats, il 100% equivale a un intero core, quindi su un host multicore sono possibili valori superiori) suggerisce un processo fuori controllo oppure risorse insufficienti per il carico di lavoro. Entrambe le condizioni vanno indagate prima del mese successivo.

Controllo degli aggiornamenti delle immagini

Le immagini di base ricevono aggiornamenti di sicurezza con cadenza irregolare. Un container che usa un’immagine aggiornata sei mesi fa potrebbe eseguire una versione di nginx o PostgreSQL con vulnerabilità note. Non scarichiamo e ridistribuiamo automaticamente tutti i container ogni mese: introdurrebbe più rischi di quanti ne ridurrebbe. Verifichiamo però cosa è in esecuzione rispetto a ciò che è aggiornato.

L’approccio pratico è questo: per ogni servizio con una versione dell’immagine fissata, una volta al mese la confrontiamo con il registro delle modifiche del progetto di origine. Per i servizi con un tag variabile come latest o 16-alpine, scarichiamo l’immagine e confrontiamo il digest per capire se è cambiata. Se è cambiata, esaminiamo le modifiche prima della distribuzione.

La disciplina più importante, però, è abbandonare i tag variabili. Un servizio ridistribuito silenziosamente con una modifica incompatibile perché il tag latest puntava a una nuova versione principale è più difficile da diagnosticare di un servizio che esegue un’immagine notoriamente vecchia. Fissa le versioni, poi aggiornale deliberatamente.

La disciplina

Nel complesso, il controllo mensile copre sei aree: utilizzo del disco e pulizia, verifica delle politiche di riavvio, rotazione dei log, scadenza dei certificati SSL, limiti delle risorse e aggiornamento delle immagini. Su un server ben documentato, queste attività richiedono in totale non più di novanta minuti. Il valore non sta nei singoli controlli, ma nel farli a cadenza fissa, prima che qualcosa si guasti.

I sistemi di produzione si degradano gradualmente. Il disco si riempie. I log crescono. I certificati invecchiano. Le immagini restano indietro. Nessuno di questi processi genera un avviso finché non viene superata una soglia. Il controllo mensile trasforma la manutenzione da reattiva a prevedibile, cambiando completamente il modo di gestire le operazioni.


Approfondimenti correlati

Per approfondire