Quando un container API parte prima del database o di Redis, il problema non è Docker in sé. Il problema è che “running” non significa “pronto”. In Compose puoi gestirlo in due modi diversi: con un healthcheck lato container, oppure con una logica di attesa nel servizio che dipende da un endpoint reale.
La differenza conta molto in ambienti con startup lente, migrazioni automatiche, limiti CPU stretti o storage remoto. Un approccio è più pulito. L’altro è più affidabile quando l’app espone un vero stato di salute.
In questo articolo confronto i due approcci con un caso concreto: una stack Compose con API Node.js, PostgreSQL e Redis, dove il container dell’API fallisce a intermittenza dopo reboot o redeploy. Il sintomo tipico è un 502 dal reverse proxy, oppure errori come connection refused nei primi secondi.
Prerequisiti
- Docker Engine e Docker Compose v2.
- Una stack con almeno due servizi dipendenti, per esempio API + PostgreSQL.
- Accesso ai log del container.
- Facoltativo ma utile: un endpoint HTTP di salute, tipo
/healtho/ready.
Note: se usi Portainer, puoi modificare la compose da Stacks e verificare i log da interfaccia. I concetti restano identici.
Step 1: capire il sintomo reale
Prima di cambiare la compose, verifica se il problema è di avvio, rete o dipendenze non pronte. Molti errori sembrano uguali, ma non lo sono.
Controlla i log dell’API e del database.
docker compose logs --tail=50 api dbapi-1 | Error: connect ECONNREFUSED 172.18.0.2:5432
api-1 | Waiting for database...
db-1 | database system is ready to accept connections
# Output: l’API tenta la connessione prima che il servizio sia davvero pronto.
Se vedi solo depends_on nella compose, ricordati che non risolve la readiness. Ordina l’avvio, ma non aspetta che PostgreSQL accetti richieste.
Step 2: approccio A con healthcheck e depends_on
Questo è l’approccio più pulito quando il servizio dipendente espone un segnale attendibile. Docker valuta lo stato del container. Compose può poi far partire l’API solo dopo che il database è healthy.
È la scelta giusta se vuoi una policy semplice, leggibile e standardizzata. Funziona bene quando il problema è solo “il servizio non è ancora pronto”.
Aggiungi un healthcheck al database.
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
api:
image: myapi:latest
depends_on:
db:
condition: service_healthy# Output: l’API parte solo dopo che PostgreSQL risponde a pg_isready.
Su Portainer il percorso è simile: vai in Stacks → apri lo stack → modifica il file YAML → salva e redeploy. Non serve toccare altro se il servizio espone già un check affidabile.
Warning: questo approccio non risolve i casi in cui il database “risponde” ma l’app non è ancora pronta a livello funzionale. Per esempio, schema non migrato, cache vuota o code non inizializzate.
Step 3: approccio B con wait script o check applicativo
Qui la dipendenza non è “il container è vivo”, ma “l’app è davvero pronta”. È la soluzione migliore quando la readiness dipende da più fattori: database, migrazioni, file di configurazione, API esterne, storage montato.
In pratica fai aspettare l’entrypoint dell’API finché un controllo reale non passa. Può essere un comando shell, oppure una richiesta HTTP a /ready.
Esempio con script semplice.
#!/bin/sh
set -e
until curl -fsS http://db:5432 >/dev/null 2>&1; do
echo "waiting for db"
sleep 2
done
exec node server.js# Output: il container dell’API resta in attesa e poi avvia Node solo quando il check passa.
Meglio ancora, se l’app ha un endpoint di readiness, usa quello.
#!/bin/sh
set -e
until curl -fsS http://127.0.0.1:3000/ready | grep -q 'ok'; do
echo "waiting for app readiness"
sleep 2
done
exec node server.jsQuesto approccio è più robusto quando hai migrazioni automatiche all’avvio. Una API può essere in ascolto ma non ancora pronta a servire traffico reale.
Note: la logica di wait è quasi sempre solo da riga di comando o dentro l’immagine. L’interfaccia grafica non può sostituirla davvero.
Step 4: scegliere in base al caso d’uso
Non usare il check applicativo ovunque. Non usare il solo healthcheck ovunque. La scelta dipende da cosa vuoi proteggere.
Quando scegliere healthcheck + depends_on
- Il servizio espone già uno stato affidabile.
- Ti basta ordinare l’avvio tra container.
- Vuoi una compose facile da leggere e mantenere.
- Hai pochi effetti collaterali all’avvio.
Quando scegliere wait script o readiness endpoint
- L’app deve completare migrazioni o inizializzazioni.
- Il servizio esterno non ha un healthcheck utile.
- Hai startup lente o intermittenti dopo reboot.
- Vuoi evitare falsi positivi: container “up” ma servizio non pronto.
Nel dubbio, usa entrambi in modo diverso: healthcheck sul database e readiness check nell’API. È la combinazione più solida in produzione.
Step 5: limiti risorse e startup lente
Molti edge case non dipendono da Compose, ma dai limiti imposti al container. Una CPU troppo stretta o poca memoria possono allungare l’inizializzazione e far fallire i check troppo aggressivi.
Se l’API ha un healthcheck interno, allunga gli intervalli quando il nodo è sotto carico.
services:
api:
image: myapi:latest
deploy:
resources:
limits:
cpus: '0.50'
memory: 512M
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:3000/ready"]
interval: 10s
timeout: 3s
retries: 12
start_period: 20s# Output: meno falsi negativi durante startup lenti o dopo picchi di carico.
Se usi Docker su host piccolo, controlla anche lo swap e il throttling. Un healthcheck troppo severo su un sistema saturo fallisce anche se il servizio recupera dopo pochi secondi.
Verifica finale
Dopo la modifica, fai un redeploy pulito e controlla l’ordine reale di partenza.
docker compose up -d --force-recreatedocker compose psdocker compose logs -f api# Output: l’API resta in attesa finché la dipendenza non è pronta, poi passa a running senza errori di connessione.
Se usi un reverse proxy come Traefik o Nginx, verifica anche il primo minuto dopo il restart. È il momento in cui emergono i problemi di readiness mascherati.
Troubleshooting
Errore: service "api" depends on service "db" which is undefined
Causa: il nome del servizio nel depends_on non coincide con la chiave YAML.
Fix:
docker compose config# Output: la compose viene validata e l’errore di nome emerge subito.
Errore: service "db" healthcheck is not supported by this image
Causa: l’immagine non contiene il comando usato nel test, oppure il binario non è nel PATH.
Fix:
docker compose exec db sh -lc 'which pg_isready || echo missing'# Output: capisci se il comando esiste davvero nel container.
Errore: api-1 exited with code 1
Causa: lo script di attesa termina prima del vero avvio, spesso per un comando troppo rigido.
Fix:
docker compose logs --tail=100 api# Output: individui il punto esatto in cui lo script fallisce e correggi il check.
Conclusione
Se ti serve solo un ordine di avvio affidabile, healthcheck + depends_on è il primo passo giusto. Se invece l’app ha una readiness reale e complessa, un wait script o un endpoint dedicato è più sicuro.
Il prossimo passo concreto è testare il reboot del nodo con la stack attiva. È il modo più veloce per scoprire se hai risolto davvero, non solo in laboratorio.
Se vuoi, nel prossimo articolo possiamo vedere come combinare healthcheck, restart policy e limiti CPU per evitare il classico effetto “container sempre up, servizio sempre rotto”.
Commenti (0)
Nessun commento ancora.
Segnala contenuto
Elimina commento
Eliminare definitivamente questo commento?
L'azione non si può annullare.