Il caso classico arriva dopo un deploy apparentemente pulito. Il container parte, il servizio risponde per qualche minuto, poi Compose lo marca unhealthy e il processo viene ucciso con exit code 137.
In mezzo ci sono quasi sempre tre colpevoli: un healthcheck troppo aggressivo, un volume montato male, oppure un limite di memoria impostato troppo basso. Se il sito è sotto traffico, il danno cresce in fretta. La parte difficile non è riavviare. È capire cosa sta fallendo, senza peggiorare il disastro con un rollback improvvisato.
Questo articolo segue uno scenario reale su Docker Compose v2.24, con un'app web dietro proxy applicativo diverso da Nginx, un database separato e un volume persistente per upload e cache. L'obiettivo è recuperare il servizio, preservare i dati e lasciare una procedura ripetibile per il prossimo incidente.
Prerequisiti
- Docker Engine 24 o superiore.
- Docker Compose v2.24 o superiore.
- Accesso alla directory del progetto Compose.
- Permessi per leggere i log dei container e per eseguire il rollback dell'immagine.
- Un volume dati separato dal filesystem del container.
Note: se usi un host con poca RAM, tieni aperto anche dmesg. Un OOMKilled spesso non appare nei log applicativi.
Step 1: fotografare lo stato, non toccare nulla
Prima di riavviare, raccogli lo stato esatto. Serve per capire se il problema è nel check, nella rete o nella memoria. Un restart cieco può cancellare il contesto utile.
docker compose ps
docker compose logs --no-color --tail=200 app
docker inspect $(docker compose ps -q app) --format '{{json .State}}' | jq
# Output: il container risulta Restarting o unhealthy, con ExitCode 137 oppure OOMKilled=true.
Perché: lo stato del runtime distingue un crash dell'app da un kill del kernel. Se vedi OOMKilled=true, il problema non è il healthcheck. È il limite risorse.
Step 2: leggere il healthcheck come un contratto, non come un ping
Molti healthcheck testano una URL interna, ma falliscono per motivi banali: DNS interno, porta sbagliata, cold start lungo, migrazione al bootstrap. Un check che parte troppo presto può far sembrare rotto un container sano.
services:
app:
image: registry.example.com/app:1.8.4
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:3000/healthz || exit 1"]
interval: 15s
timeout: 3s
retries: 5
start_period: 45s
# Output: il container resta starting per 45 secondi, poi diventa healthy se l'endpoint risponde.
Se il tuo check dipende dalla rete Compose, verifica anche il resolver interno. Un container può essere vivo ma non raggiungere il nome del servizio database.
docker exec -it $(docker compose ps -q app) sh -lc 'getent hosts db; curl -fsS http://db:5432 || true'
# Output: il nome db viene risolto oppure no. Se non risolve, il problema è nella rete o nel service name.
Warning: non usare healthcheck che scrivono file temporanei nel container. Con un filesystem in sola lettura o con permessi stretti, falliscono in modo intermittente.
Step 3: distinguere volume rotto da permessi rotti
Quando un volume è montato male, l'app spesso sembra avviarsi, ma poi va in errore al primo accesso ai dati. In altri casi il volume esiste, ma l'UID del processo non coincide con il proprietario delle directory.
docker inspect $(docker compose ps -q app) --format '{{range .Mounts}}{{println .Source "->" .Destination}}{{end}}'
ls -lah /srv/app-data
stat -c '%u:%g %n' /srv/app-data /srv/app-data/uploads
# Output: vedi il percorso host montato, il proprietario delle directory e un eventuale mismatch con l'utente del container.
Se il container gira come UID 1001 e il volume è root:root, l'app può fallire con Permission denied dopo un write iniziale. La correzione deve essere fatta sul dato persistente, non nel layer effimero del container.
chown -R 1001:1001 /srv/app-data
chmod -R u+rwX,go-rwx /srv/app-data
# Output: l'app torna a scrivere nei path persistenti senza errori di permesso.
Note: se il volume è gestito da driver esterni, verifica prima il driver stesso. Un mount NFS o CIFS può introdurre latenza e locking che sembrano problemi applicativi.
Step 4: controllare i limiti di memoria e CPU prima del prossimo crash
Un exit code 137 significa quasi sempre kill forzato. Su host con overcommit aggressivo, il kernel può terminare il processo anche se il container non mostra errori chiari nei log.
services:
app:
mem_limit: 768m
cpus: "1.50"
deploy:
resources:
limits:
memory: 768M
# Output: il container riceve un tetto di memoria coerente con il carico atteso.
Su Compose non orchestrato, la sezione deploy non è sempre applicata come in Swarm. Usa i limiti supportati dal tuo setup reale e verifica con l'inspection del container.
docker inspect $(docker compose ps -q app) --format 'Memory={{.HostConfig.Memory}} NanoCPUs={{.HostConfig.NanoCpus}}'
# Output: leggi i limiti effettivi applicati dal runtime, non quelli scritti nel file YAML.
Se il processo usa picchi di memoria durante le migrazioni o la compilazione degli asset, alza il limite o sposta quella fase fuori dal container runtime. Un healthcheck non deve competere con un job pesante.
Step 5: fare rollback dell'immagine senza perdere il volume
Se il problema nasce dal codice o da una dipendenza nuova, il rollback deve cambiare solo l'immagine. Il volume dati va lasciato intatto. È il punto più importante quando l'app è già in produzione.
services:
app:
image: registry.example.com/app:1.8.3
# Output: il servizio riparte con la versione precedente dell'immagine, mantenendo gli stessi mount e la stessa rete.
Prima del rollback, annota il digest o il tag precedente. Se usi tag mobili come latest, il rollback non è davvero ripetibile.
docker compose pull app
docker compose up -d --no-deps app
docker compose ps
# Output: il container viene sostituito senza toccare gli altri servizi del progetto.
Warning: non usare down -v in emergenza, a meno che tu non voglia cancellare anche i dati persistenti. Per un incidente di healthcheck o memoria, è quasi sempre la scelta sbagliata.
Step 6: ripristino controllato del volume dopo una corruzione parziale
Se il volume contiene file troncati o cache incoerente, il container può riavviarsi ma restare inutilizzabile. Qui serve distinguere dati critici e dati rigenerabili.
docker compose stop app
mv /srv/app-data/cache /srv/app-data/cache.bak.$(date +%F-%H%M)
mkdir -p /srv/app-data/cache
chown -R 1001:1001 /srv/app-data/cache
# Output: la cache viene ricreata pulita, mentre il resto dei dati resta disponibile.
Per upload, media e database embedded il discorso cambia. Quelli non si rinominano alla leggera. Serve un backup verificato prima di qualsiasi modifica.
Se il dato è rigenerabile, isola la cartella e ricostruiscila. Se il dato è business-critical, fermati e ripristina da snapshot o backup validato.
Step 7: verificare la rete interna dopo il ripristino
Un container può sembrare sano in locale, ma non vedere il database o il broker sulla rete Compose. Dopo un rollback, questa è una delle regressioni più comuni.
docker network ls
docker network inspect $(docker compose ls --format json | jq -r '.[0].Name') | jq '.[0].Containers'
docker exec -it $(docker compose ps -q app) sh -lc 'nc -vz db 5432; nc -vz redis 6379'
# Output: i servizi interni rispondono sulle porte attese e il container è connesso alla rete corretta.
Se il nome del progetto Compose cambia tra ambienti, cambia anche il nome della rete. In quel caso un restore manuale può lasciare il container isolato. Il sintomo tipico è un timeout, non un errore esplicito.
Verifica finale
La verifica finale deve confermare tre cose: il servizio è healthy, i dati persistenti sono leggibili e il consumo risorse è sotto soglia.
docker compose ps
curl -fsS https://app.example.com/healthz
docker stats --no-stream $(docker compose ps -q app)
# Output: stato healthy, risposta HTTP 200 e memoria stabile senza crescita anomala.
Se hai un proxy esterno o un load balancer, controlla anche il back-end registrato. Un container sano ma non registrato nel pool produce falsi positivi in dashboard e allarmi.
Troubleshooting
1) service app is unhealthy
Causa: l'endpoint di healthcheck non è pronto entro start_period o usa una dipendenza non disponibile all'avvio.
Fix:
docker compose logs --tail=100 app
sed -i 's/start_period: 45s/start_period: 90s/' docker-compose.yml
docker compose up -d app
# Output: il container ha più tempo per completare bootstrap e migrazioni.
2) OCI runtime error: container_linux.go:...: memory limit exceeded
Causa: il processo supera il limite memoria configurato nel container.
Fix:
docker inspect $(docker compose ps -q app) --format '{{.HostConfig.Memory}}'
# poi aumenta il limite nel compose
docker compose up -d --no-deps --force-recreate app
# Output: il nuovo container parte con un tetto memoria coerente.
3) permission denied while trying to connect to the Docker daemon socket
Causa: il comando di ripristino è stato lanciato da un utente senza accesso al socket Docker.
Fix:
sudo usermod -aG docker $USER
newgrp docker
docker compose ps
# Output: i comandi Compose tornano eseguibili senza sudo continuo.
Conclusione
Quando un container muore con exit code 137, il problema raramente è solo l'app. Devi leggere insieme healthcheck, volumi, rete e limiti risorse.
Il prossimo passo concreto è trasformare questa procedura in una checklist post-deploy. Aggiungi un test di memoria, un test di scrittura sul volume e un controllo DNS interno prima di esporre il traffico reale.
Se vuoi ridurre ancora il rischio, prepara un tag di rollback già verificato e un backup puntuale del volume dati prima di ogni rilascio.
Commenti (0)
Nessun commento ancora.
Segnala contenuto
Elimina commento
Eliminare definitivamente questo commento?
L'azione non si può annullare.