Risoluzione dei problemi: Diagnosi rapida degli errori comuni dei container Docker

Padroneggia l'arte della rapida risoluzione dei problemi dei container Docker con questa guida essenziale. Impara il processo strutturato per diagnosticare i fallimenti di avvio utilizzando i comandi Docker principali. Spieghiamo come sfruttare `docker ps -a` per identificare i crash, estrarre informazioni critiche con `docker logs` ed eseguire analisi avanzate della configurazione con `docker inspect`. Questo articolo fornisce esempi pratici e soluzioni mirate per problemi frequenti, inclusi errori con codice di uscita 127, conflitti di porta ed eventi OOMKilled, assicurandoti di poter identificare rapidamente la causa principale e ripristinare il servizio.

Risoluzione dei problemi: Diagnosi rapida degli errori comuni dei container Docker

Quando un container Docker esce immediatamente, non iniziare ricostruendo l'immagine o cambiando flag a caso. Inizia scoprendo cosa sa Docker: lo stato del container, il codice di uscita, i log e il comando esatto che Docker ha tentato di eseguire. Questi quattro elementi di solito restringono rapidamente il problema.

Un container è solo un processo con isolamento attorno. Se il processo principale esce, il container esce. Può essere un crash, un eseguibile mancante, un job batch completato, una dipendenza di health check fallita o il kernel che uccide il processo perché ha usato troppa memoria. I comandi seguenti ti aiutano a distinguere questi casi.

Trova prima il container fermo

docker ps mostra solo i container in esecuzione. I container che non sono riusciti ad avviarsi sono solitamente nascosti a meno che non chiedi tutti i container:

docker ps -a

Guarda STATUS, COMMAND e NAMES:

CONTAINER ID   IMAGE          COMMAND              STATUS                      NAMES
2d3f4b5c6e7a   my-app:latest  "/usr/bin/start"     Exited (127) 2 minutes ago  web-service
91aa34c0db22   worker:latest  "python worker.py"   Exited (0) 10 minutes ago   nightly-worker

Exited (0) spesso significa che il processo è stato completato con successo. Questo è normale per job one-shot. Per un servizio web, può significare che il comando è stato eseguito e terminato invece di rimanere in primo piano.

I codici di uscita diversi da zero indicano un fallimento, ma trattali come indizi piuttosto che come risposte definitive. Il codice di uscita 127 comunemente significa comando non trovato. 126 comunemente significa trovato ma non eseguibile. 137 spesso significa che il processo ha ricevuto SIGKILL; nei container questo è frequentemente, ma non sempre, correlato alla pressione della memoria. Conferma sempre con log e output di inspect.

Leggi i log prima di cambiare qualsiasi cosa

Docker cattura stdout e stderr dal processo principale del container per il driver di logging predefinito. Usa:

docker logs web-service

Opzioni utili:

docker logs --tail 100 web-service
docker logs --since 15m web-service
docker logs -t web-service
docker logs -f web-service

Se i log dicono config file not found, controlla i mount e l'ambiente. Se mostrano un stack trace dell'applicazione, esegui il debug dell'applicazione. Se sono vuoti, il processo potrebbe essere fallito prima di produrre output, o l'entrypoint dell'immagine potrebbe essere sbagliato.

Per un crash loop, evita docker logs -f come tuo unico strumento. Può far sembrare il fallimento attivo senza darti lo stato. Abbina i log con docker inspect.

Ispeziona lo stato del container

docker inspect restituisce un grande documento JSON. Raramente ti serve tutto. Inizia con campi formattati:

docker inspect -f 'status={{.State.Status}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}} error={{.State.Error}}' web-service

Poi ispeziona il comando e la configurazione dell'immagine:

docker inspect -f 'entrypoint={{json .Config.Entrypoint}} cmd={{json .Config.Cmd}} user={{.Config.User}}' web-service

Controlla i mount quando l'errore coinvolge file:

docker inspect -f '{{json .Mounts}}' web-service

Se il container è stato ucciso per memoria, .State.OOMKilled è il campo importante. Se è true, aumentare la memoria può aiutare, ma la domanda migliore successiva è perché la memoria è cresciuta. Un limite più grande può nascondere una perdita abbastanza a lungo da fallire più tardi.

Riproduci con una shell interattiva quando possibile

Se l'immagine contiene una shell, sovrascrivi l'entrypoint e ispeziona il filesystem:

docker run --rm -it --entrypoint /bin/sh my-app:latest

Alcune immagini hanno Bash:

docker run --rm -it --entrypoint /bin/bash my-app:latest

All'interno, controlla i file e i percorsi dei comandi:

ls -l /usr/bin/start
id
env

Le immagini minime potrebbero non includere una shell. In tal caso, usa gli strumenti disponibili dell'immagine, ricostruisci una variante di debug temporanea o ispeziona il Dockerfile e l'output della build. Non aggiungere permanentemente pacchetti di debug a un'immagine di produzione solo perché la risoluzione dei problemi è stata scomoda una volta.

Comando non trovato: exit 127

L'uscita 127 di solito significa che Docker non ha potuto trovare l'eseguibile nominato da ENTRYPOINT o CMD, o uno script di avvio ha tentato di eseguire un comando mancante.

Cause comuni:

  • L'eseguibile non è mai stato copiato nell'immagine.
  • Il percorso è corretto sull'host ma non all'interno dell'immagine.
  • Lo script usa /bin/bash, ma l'immagine ha solo /bin/sh.
  • Il comando dipende da PATH, e PATH differisce da quello che ti aspetti.

Controlla il comando dell'immagine:

docker inspect -f '{{json .Config.Entrypoint}} {{json .Config.Cmd}}' web-service

Se l'entrypoint è uno script, controlla il suo shebang e le terminazioni di riga. Uno script con terminazioni CRLF di Windows può fallire con messaggi confusi di "non trovato" perché il percorso dell'interprete contiene effettivamente un carriage return.

Permesso negato: exit 126 o errori di file

L'uscita 126 spesso significa che Docker ha trovato il comando ma non ha potuto eseguirlo. Per gli script, il file potrebbe non avere il bit eseguibile:

COPY start.sh /usr/local/bin/start.sh
RUN chmod 0755 /usr/local/bin/start.sh
ENTRYPOINT ["/usr/local/bin/start.sh"]

Per i file montati da volume, ricorda che si applicano i permessi dell'host. Se un container viene eseguito come UID 1000 e la directory dell'host è di proprietà di root senza permesso di scrittura, il container non può scrivere lì solo perché è "dentro Docker".

Controlla l'utente runtime:

docker inspect -f 'user={{.Config.User}}' web-service

Se è vuoto, molte immagini vengono eseguite come root per impostazione predefinita, ma non tutte. Le immagini ufficiali e hardened per la sicurezza usano spesso un utente non root.

Porta già allocata

Un errore di bind di solito appare quando pubblichi una porta host che è già in uso:

docker run -p 8080:80 nginx

Docker potrebbe segnalare qualcosa come bind: address already in use. Trova il conflitto:

docker ps --format 'table {{.Names}}\t{{.Ports}}'
lsof -iTCP:8080 -sTCP:LISTEN

Quindi ferma il processo in conflitto o scegli un'altra porta host:

docker run -p 8081:80 nginx

La porta del container può rimanere la stessa. La porta host è la parte prima dei due punti.

File mancanti e mount errati

Se i log dicono che un file di configurazione è mancante, confronta ciò che l'applicazione si aspetta con ciò che Docker ha montato:

docker inspect -f '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}' web-service

Un errore comune è montare una directory host su un percorso che già aveva file nell'immagine. Il mount nasconde i contenuti dell'immagine in quella destinazione. Se l'immagine contiene /app/config/default.yml e monti una directory host vuota su /app/config, il file predefinito scompare dalla vista del container.

Controlla anche i percorsi relativi. -v ./config:/app/config dipende dalla directory in cui hai eseguito docker run, non dalla directory in cui si trova il Dockerfile.

I fallimenti degli health check non sono sempre crash del container

Un container può essere in esecuzione ma non sano:

docker ps

Potresti vedere Up 2 minutes (unhealthy). Ispeziona l'output dell'health check:

docker inspect -f '{{json .State.Health}}' web-service

Gli health check spesso falliscono perché l'app è in ascolto su una porta diversa, si lega solo a 127.0.0.1, impiega più tempo ad avviarsi di quanto l'health check permetta, o ha bisogno di un database che non è ancora pronto. Non confondere un container non sano con uno uscito; il percorso diagnostico è diverso.

Una sequenza rapida di risoluzione dei problemi

Usa questo ordine quando hai bisogno di una risposta rapidamente:

  1. docker ps -a per trovare il container e il codice di uscita.
  2. docker logs --tail 100 <nome> per leggere l'errore dell'applicazione.
  3. docker inspect -f ... per controllare stato, comando, utente e mount.
  4. Esegui una shell temporanea nell'immagine se il comando o il filesystem sono sospetti.
  5. Controlla i conflitti host per porte e permessi delle directory montate.
  6. Ricostruisci solo dopo aver saputo se il problema è il contenuto dell'immagine, i flag runtime o la configurazione dell'applicazione.

Questa sequenza mantiene l'indagine concreta. Docker di solito ha abbastanza prove; il trucco è leggerle prima di cambiare la scena.

Controlla la politica di riavvio prima di fidarti di ciò che vedi

Una politica di riavvio può far sembrare un container in continuo fallimento o in continuo recupero. Controllala:

docker inspect -f 'restart={{json .HostConfig.RestartPolicy}}' web-service

Se la politica è always o unless-stopped, Docker potrebbe riavviare il container dopo ogni crash. docker ps potrebbe mostrarlo in esecuzione per pochi secondi, poi riavviarsi di nuovo. In tal caso, usa i log con timestamp e ispeziona il conteggio dei riavvii:

docker inspect -f 'restarts={{.RestartCount}} started={{.State.StartedAt}} finished={{.State.FinishedAt}}' web-service

Un conteggio di riavvii elevato di solito significa che il processo principale esce rapidamente. La soluzione è raramente "cambiare la politica di riavvio". La politica sta solo rivelando il fallimento sottostante.

Distinguere i problemi di build-time da quelli di run-time

Se un file manca all'interno del container, chiediti quando dovrebbe essere apparso. I file copiati nel Dockerfile sono problemi di build-time. I file montati con -v o volumi Compose sono problemi di run-time.

Controlli di build-time:

docker image inspect my-app:latest
docker run --rm --entrypoint /bin/sh my-app:latest -c 'ls -la /app'

Controlli di run-time:

docker inspect -f '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}' web-service

Questa divisione fa risparmiare tempo. Ricostruire l'immagine non risolverà un mount host errato. Cambiare un flag di volume non risolverà un Dockerfile che non ha mai copiato il binario.

Le variabili d'ambiente e i segreti possono fallire silenziosamente

Molte applicazioni escono perché una variabile d'ambiente richiesta è mancante, ma l'errore Docker dice solo che il processo è uscito con codice 1. Ispeziona attentamente l'ambiente configurato:

docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' web-service

Fai attenzione a dove esegui quel comando; può stampare segreti. Nei log condivisi, stampa solo i nomi delle variabili:

docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' web-service | sed 's/=.*//'

Se usi --env-file, controlla le terminazioni CRLF, gli spazi non quotati e i file mancanti. I file env di Docker non sono script shell completi. Mantienili semplici: righe KEY=value, commenti dove supportati dalla tua versione di Docker e nessuna assunzione che l'espansione della shell avvenga all'interno del file.

Una revisione realistica prima di distribuire

Prima di considerare uno script o una configurazione di container come finita, leggila una volta come se fossi la prossima persona che deve eseguirne il debug alle 2 del mattino. Questo cambia ciò che noti. Un prompt che aveva senso mentre scrivevi lo script potrebbe essere ambiguo quando appare in un log CI. Un nome di servizio Docker che sembrava ovvio potrebbe non corrispondere al nome della variabile nell'applicazione. Un valore predefinito di Bash potrebbe essere sicuro per lo sviluppo e pericoloso per la produzione.

Mi piace fare un breve dry run con valori deliberatamente scomodi. Usa un percorso con spazi. Usa un valore opzionale vuoto. Prova un nome di file che inizia con un trattino. Esegui lo script da una directory di lavoro diversa. Avvia il container senza una variabile d'ambiente prevista. Questi test non sono fantasiosi, ma catturano le assunzioni che di solito si rompono per prime.

Controlla anche il messaggio di fallimento. Se l'unico output è failed, il consiglio dell'articolo non è stato implementato. Un fallimento utile dice quale valore è stato usato, quale controllo è fallito e cosa l'operatore può cambiare. Questo non significa scaricare ogni variabile d'ambiente o stampare segreti. Significa essere specifici dove la specificità aiuta: il percorso di configurazione, il nome del comando mancante, il nome della rete, l'hostname del servizio o la porta a cui il processo ha tentato di legarsi.

L'abitudine finale è mantenere gli esempi vicini al modo in cui il sistema viene effettivamente eseguito. Se la produzione usa Compose, testa con Compose. Se uno script viene avviato da systemd, testalo con systemd o con un ambiente altrettanto minimo. Se un comando dovrebbe essere sicuro per copia e incolla, includi le virgolette, i separatori -- e la convalida nell'esempio stesso. I lettori copiano schemi funzionanti più spesso di quanto copino avvisi.

Quella revisione non è burocrazia. È come la piccola automazione rimane noiosa. Noioso è ciò che vuoi da prompt shell, caricatori di configurazione, espansione di variabili, diagnostica dei container e networking Docker. Meno sorprendente è il comportamento, più facile è per il prossimo operatore fidarsene.

Per i fallimenti Docker, salva il comando esatto che ha creato il container quando puoi. docker inspect può mostrare la configurazione corrente, ma una nota di incidente che include il comando docker run originale, il servizio Compose, il tag dell'immagine e il nome del file env è molto più facile da riprodurre in seguito. Evita di usare latest durante il debug serio. Un tag mobile può trasformare un fallimento in due indagini diverse perché l'immagine cambia mentre stai ancora leggendo i log.

Se stai diagnosticando un problema simile alla produzione localmente, tira la stessa immagine digest o tag, usa lo stesso layout di volume e passa la stessa forma di ambiente non segreto. La riproduzione batte la speculazione.