Limiti di risorse, healthcheck e politiche di riavvio in Docker Compose
Il tuo file Compose funziona in sviluppo ma non è pronto per la produzione. Impara ad aggiungere limiti di memoria/CPU, healthcheck, politiche di riavvio e ordinamento dell'avvio per proteggere il tuo VPS da OOM kill e guasti a cascata.
Un file Compose che esegue docker compose up senza errori non è pronto per la produzione. Senza limiti di risorse, un singolo container può consumare tutta la memoria dell'host e attivare il OOM killer di Linux, facendo cadere ogni servizio sul VPS. Senza healthcheck, Docker non ha modo di rilevare un processo bloccato. Senza politiche di riavvio, un container crashato resta morto finché non te ne accorgi.
Questi tre sistemi lavorano insieme: i limiti di risorse impediscono il consumo incontrollato, gli healthcheck rilevano i guasti e le politiche di riavvio gestiscono il ripristino. Questa guida copre tutti e tre come livello integrato di hardening per Docker Compose v2.
Prerequisiti: un'installazione funzionante di Docker Compose su un VPS. Familiarità con la struttura base di un file Compose. Per un ripasso, consulta .
Come si impostano i limiti di memoria e CPU in Docker Compose?
Usa la chiave deploy.resources.limits nella definizione del servizio. Imposta memory con un valore come 512M o 1G per creare un tetto rigido. Imposta cpus con una stringa decimale come '0.5' per mezzo core. Il processo del container viene terminato dall'OOM killer se supera il limite di memoria. I limiti CPU rallentano il processo invece di terminarlo.
services:
api:
image: node:22-slim
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 256M
Cosa fa: il container api può usare al massimo 1 core CPU e 512 MB di RAM. Docker garantisce almeno 0,25 core e 256 MB disponibili, anche quando altri container competono per le risorse.
Verifica che i limiti siano applicati:
docker compose up -d
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}"
La colonna MEM USAGE / LIMIT mostra sia l'utilizzo corrente che il tetto configurato. Dovresti vedere 512MiB come limite, non la RAM totale dell'host. Se vedi la memoria totale dell'host, i limiti non sono attivi.
Qual è la differenza tra limits e reservations?
I limits sono tetti rigidi. Il container non può superarli. Le reservations sono garanzie flessibili. Docker le usa per le decisioni di pianificazione e la gestione della pressione sulla memoria.
| Impostazione | Funzione | Quando è rilevante |
|---|---|---|
limits.memory |
Tetto rigido. OOM kill se superato. | Sempre. Previene container fuori controllo. |
limits.cpus |
Limitazione. Il processo rallenta. | Carichi pesanti di CPU (build, inferenza). |
reservations.memory |
Minimo garantito. | Host sotto pressione di memoria. |
reservations.cpus |
Quota CPU garantita. | Più container ad alto uso CPU. |
Senza reservations, Docker alloca risorse in ordine di arrivo. Sotto pressione, qualsiasi container può restare senza risorse. Imposta le reservations al minimo necessario per il funzionamento del servizio.
Cosa succede quando un container Docker supera il suo limite di memoria?
Il OOM killer del kernel Linux termina il processo principale del container con SIGKILL. Docker registra il codice di uscita 137 (128 + 9, dove 9 è SIGKILL). Se la politica di riavvio è on-failure o always, Docker riavvia il container automaticamente.
Controlla se un container è stato vittima di un OOM kill:
docker inspect api-1 --format '{{.State.OOMKilled}}'
Output: true conferma un OOM kill.
Per maggiori dettagli:
docker inspect api-1 --format '{{json .State}}' | python3 -m json.tool
Cerca "OOMKilled": true, "ExitCode": 137 e "RestartCount" per vedere quante volte è stato riavviato.
Senza limiti, il container alloca memoria finché l'host non si esaurisce. Il kernel allora termina processi a livello di sistema per liberare RAM. Questo può far cadere il tuo database, reverse proxy o daemon SSH. I limiti confinano il raggio d'impatto al container problematico.
Come pianificare il budget delle risorse dei container su un VPS?
Su un VPS con risorse fisse, devi distribuire la memoria tra tutti i container. Lascia margine per il sistema operativo dell'host e Docker stesso.
Esempio di budget per un VPS da 8 GB:
| Componente | Memoria |
|---|---|
| SO host + motore Docker | 1 GB |
| PostgreSQL | 2 GB |
| Redis | 512 MB |
| API Node.js | 1 GB |
| Nginx | 128 MB |
| Worker in background | 1 GB |
| Margine (non allocato) | 2,35 GB |
Tieni il 20-30% non allocato come margine. Se la somma dei limiti dei tuoi container supera la RAM totale dell'host, rischi che il OOM killer dell'host intervenga, ignorando i confini dei container Docker.
Verifica l'allocazione totale rispetto alla memoria dell'host:
free -h
docker stats --no-stream --format "{{.Name}}: {{.MemUsage}}"
Come si configura un healthcheck in Docker Compose?
Aggiungi un blocco healthcheck alla definizione del servizio. Docker esegue il comando di test all'intervallo specificato e contrassegna il container come unhealthy dopo il numero configurato di fallimenti consecutivi. Altri servizi possono dipendere da questo stato di salute per l'ordinamento dell'avvio.
services:
api:
image: node:22-slim
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
start_interval: 2s
Parametri dell'healthcheck:
| Parametro | Default | Consigliato | Cosa controlla |
|---|---|---|---|
interval |
30s | 15-60s | Tempo tra i controlli dopo l'avvio |
timeout |
30s | 5-10s | Durata massima di un controllo |
retries |
3 | 3-5 | Fallimenti prima di unhealthy |
start_period |
0s | 10-60s | Periodo di grazia per servizi ad avvio lento |
start_interval |
5s | 2-5s | Intervallo durante l'avvio (Compose v2.20+) |
Il parametro start_interval (aggiunto in Compose v2.20) permette controlli più frequenti durante l'avvio. Il container passa da starting a healthy appena il primo controllo ha successo durante lo start_period. Dopo, i controlli vengono eseguiti al normale interval.
Qual è la differenza tra CMD e CMD-SHELL negli healthcheck?
CMD esegue il comando direttamente senza shell. CMD-SHELL lo passa attraverso /bin/sh -c. Usa CMD quando possibile. Evita l'overhead della shell ed elimina i problemi di PID 1 dove la shell, non il tuo comando di controllo, riceve i segnali.
# Formato CMD - nessuna shell, esegue il binario direttamente
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
# Formato CMD-SHELL - passa attraverso /bin/sh -c
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
# Scorciatoia stringa - equivalente a CMD-SHELL
healthcheck:
test: curl -f http://localhost:3000/health || exit 1
Usa CMD-SHELL quando hai bisogno di funzionalità della shell come ||, pipe o espansione di variabili. Usa CMD per la semplice esecuzione di binari.
Quale comando di healthcheck usare per PostgreSQL, Redis e Nginx?
Ogni servizio ha bisogno di un healthcheck che testi la capacità del servizio di gestire richieste, non solo se il processo è in esecuzione.
| Servizio | Comando healthcheck | Cosa testa |
|---|---|---|
| PostgreSQL | ["CMD", "pg_isready", "-U", "postgres"] |
Accetta connessioni |
| Redis | ["CMD", "redis-cli", "ping"] |
Risponde ai comandi |
| Nginx | `["CMD-SHELL", "curl -f http://localhost/ | |
| App Node.js | `["CMD-SHELL", "curl -f http://localhost:3000/health | |
| MySQL/MariaDB | ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] |
Motore pronto, non solo socket aperto |
Importante: per gli healthcheck basati su curl, l'immagine deve includere curl. Molte immagini slim non lo hanno. Installalo nel tuo Dockerfile, usa wget in alternativa, o scrivi un endpoint di salute minimale che il tuo framework applicativo serve nativamente.
Verifica lo stato dell'healthcheck:
docker compose ps
Cerca (healthy) o (unhealthy) nella colonna STATUS. Per lo storico dettagliato:
docker inspect api-1 --format '{{json .State.Health}}' | python3 -m json.tool
Questo mostra gli ultimi risultati dei controlli, incluso l'output stdout/stderr dei controlli falliti. Fai attenzione: se FailingStreak continua a incrementarsi, il tuo comando di controllo è sbagliato o il servizio ha un problema reale.
Come interagiscono le politiche di riavvio con gli healthcheck?
Le politiche di riavvio controllano cosa fa Docker quando un container si ferma. Gli healthcheck controllano come Docker rileva problemi nei container in esecuzione. Insieme, creano un ciclo di ripristino automatico: l'healthcheck rileva il guasto, il container viene fermato e la politica di riavvio lo riporta in vita.
services:
api:
restart: on-failure:5
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
Confronto delle politiche di riavvio
| Politica | In caso di crash | Al riavvio | Su docker stop |
Ideale per |
|---|---|---|---|---|
no |
Resta morto | Resta morto | Resta morto | Attività una tantum, debug |
always |
Riavvia | Riavvia | Riavvia | Infrastruttura di base (database, proxy) |
unless-stopped |
Riavvia | Riavvia | Resta morto | La maggior parte dei servizi in produzione |
on-failure:N |
Riavvia (fino a N volte) | Resta morto | Resta morto | Servizi che non devono riavviarsi all'infinito |
on-failure:5 significa che Docker riprova fino a 5 volte. Dopo, il container resta morto. Questo previene cicli di riavvio che sprecano CPU su un servizio fondamentalmente rotto.
L'interazione OOM + riavvio: quando un container raggiunge il suo limite di memoria e subisce un OOM kill (codice di uscita 137), Docker lo tratta come un fallimento. Con on-failure:5, riavvia fino a 5 volte. Se il servizio ha un memory leak, verrà terminato e riavviato ripetutamente fino al raggiungimento del limite di tentativi. Controlla il contatore dei riavvii:
docker inspect api-1 --format '{{.RestartCount}}'
Per la maggior parte dei servizi in produzione, usa unless-stopped. Riavvia in caso di crash e riavvio dell'host, ma rispetta i comandi manuali docker compose stop. Usa on-failure:N per servizi dove un ciclo di crash deve attivare un allarme, non riprovare silenziosamente all'infinito.
Come far attendere un servizio che un altro sia sano?
Usa depends_on con condition: service_healthy. Questo fa sì che Docker Compose aspetti che l'healthcheck della dipendenza passi prima di avviare il servizio dipendente.
services:
db:
image: postgres:17
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
restart: unless-stopped
api:
image: myapp:latest
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
redis:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 3
restart: unless-stopped
Senza condition: service_healthy, depends_on aspetta solo che il container si avvii, non che il servizio al suo interno sia pronto. PostgreSQL impiega diversi secondi per inizializzarsi. La tua applicazione crasherebbe tentando di connettersi a un database che non accetta ancora connessioni.
L'opzione restart: true dentro depends_on (Compose v2.21+) indica a Docker di riavviare il servizio dipendente se la dipendenza si riavvia:
depends_on:
db:
condition: service_healthy
restart: true
Questo è utile quando la tua applicazione tiene in cache la connessione al database e non riesce a recuperare da un riavvio del database senza un riavvio completo.
Quali ulimits impostare per i container in produzione?
Imposta nofile (descrittori di file aperti) e nproc (numero massimo di processi) per servizi che gestiscono molte connessioni simultanee. Ogni connessione TCP, file aperto e pipe consuma un descrittore di file. Il limite predefinito (1024 in molte immagini) è troppo basso per database e servizi ad alto traffico.
services:
db:
image: postgres:17
ulimits:
nofile:
soft: 65536
hard: 65536
nproc:
soft: 4096
hard: 4096
Verifica all'interno del container:
docker compose exec db cat /proc/1/limits
Cerca Max open files e Max processes. I valori devono corrispondere a quelli impostati.
Prevenzione dei fork bomb con il limite di PID
Imposta deploy.resources.limits.pids per limitare il numero di processi che un container può creare. Questo previene che fork bomb e la creazione incontrollata di processi consumino tutti i PID dell'host.
services:
api:
image: myapp:latest
deploy:
resources:
limits:
pids: 200
Se non usi deploy.resources per i limiti CPU/memoria, funziona anche la chiave di primo livello pids_limit. Ma quando deploy.resources.limits è presente, devi mettere anche il limite di PID lì. Mescolare le due opzioni causa un errore di validazione in Compose v5+.
200 PID sono generosi per una tipica applicazione web. Un'app Node.js ne usa circa 10-30. PostgreSQL usa circa un processo per connessione più worker in background. Dimensiona a 2-3 volte il tuo picco previsto.
Come limitare la dimensione dei log dei container?
Senza limiti sui log, i log dei container crescono senza controllo. Un servizio verboso può riempire il disco in poche ore. Imposta max-size e max-file sul driver di logging json-file per ruotare i log automaticamente.
services:
api:
image: myapp:latest
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Questo mantiene al massimo 3 file da 10 MB ciascuno, limitando lo storage dei log a 30 MB per servizio. Regola in base alle tue esigenze di debug. Usa un'ancora YAML per applicare la stessa configurazione di logging a tutti i servizi:
x-logging: &default-logging
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
services:
api:
image: myapp:latest
logging: *default-logging
worker:
image: myapp:latest
logging: *default-logging
Impostare stop_grace_period per uno spegnimento pulito
Quando Docker ferma un container, invia SIGTERM e aspetta che il processo termini in modo ordinato. Se il processo non termina entro il periodo di grazia, Docker invia SIGKILL. Il valore predefinito è 10 secondi.
services:
db:
image: postgres:17
stop_grace_period: 30s
api:
image: myapp:latest
stop_grace_period: 5s
I database hanno bisogno di periodi di grazia più lunghi per scaricare le scritture e chiudere le connessioni in modo pulito. I web server e i processi API tipicamente terminano in pochi secondi. Imposta il periodo di grazia in base al tempo effettivo di spegnimento del tuo servizio, con un piccolo margine.
File Compose completo pronto per la produzione
Questo esempio combina tutte le impostazioni per uno stack applicativo web tipico: reverse proxy Nginx, API Node.js, database PostgreSQL e cache Redis.
x-logging: &default-logging
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
services:
nginx:
image: nginx:1.27-alpine
ports:
- "80:80"
- "443:443"
deploy:
resources:
limits:
cpus: '0.5'
memory: 128M
reservations:
memory: 64M
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"]
interval: 30s
timeout: 5s
retries: 3
restart: unless-stopped
stop_grace_period: 5s
logging: *default-logging
depends_on:
api:
condition: service_healthy
api:
image: myapp:latest
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
pids: 200
reservations:
cpus: '0.25'
memory: 256M
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
start_interval: 2s
restart: unless-stopped
stop_grace_period: 5s
logging: *default-logging
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
db:
image: postgres:17
environment:
POSTGRES_PASSWORD_FILE: /run/secrets/db_password
secrets:
- db_password
deploy:
resources:
limits:
cpus: '1.0'
memory: 2G
reservations:
cpus: '0.5'
memory: 1G
healthcheck:
test: ["CMD", "pg_isready", "-U", "postgres"]
interval: 10s
timeout: 3s
retries: 5
start_period: 15s
restart: unless-stopped
stop_grace_period: 30s
ulimits:
nofile:
soft: 65536
hard: 65536
logging: *default-logging
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
memory: 128M
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
restart: unless-stopped
stop_grace_period: 5s
logging: *default-logging
volumes:
pgdata:
secrets:
db_password:
file: ./secrets/db_password.txt
Nota bene: la password del database usa Docker secrets con POSTGRES_PASSWORD_FILE invece di una variabile d'ambiente POSTGRES_PASSWORD in chiaro. Crea il file dei secrets con permessi ristretti:
mkdir -p secrets
openssl rand -base64 32 > secrets/db_password.txt
chmod 600 secrets/db_password.txt
Lista di verifica
Dopo aver applicato tutte le impostazioni, passa in rassegna questi controlli per confermare che tutto sia attivo.
1. Verificare che i limiti di risorse siano applicati:
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}"
La colonna MEM USAGE / LIMIT mostra sia l'utilizzo corrente che il tetto configurato. Ogni container deve mostrare il proprio limite di memoria configurato, non la RAM totale dell'host.
2. Verificare lo stato degli healthcheck:
docker compose ps
Tutti i servizi devono mostrare (healthy) nella colonna STATUS. Se qualcuno mostra (health: starting), attendi il completamento dello start_period.
3. Verificare la politica di riavvio:
docker inspect --format '{{.HostConfig.RestartPolicy.Name}}:{{.HostConfig.RestartPolicy.MaximumRetryCount}}' $(docker compose ps -q)
4. Verificare gli ulimits dentro un container:
docker compose exec db cat /proc/1/limits | grep -E "open files|processes"
5. Verificare la configurazione dei log:
docker inspect --format '{{.HostConfig.LogConfig.Type}} max-size={{index .HostConfig.LogConfig.Config "max-size"}} max-file={{index .HostConfig.LogConfig.Config "max-file"}}' $(docker compose ps -q api)
6. Testare la catena di ripristino completa:
Ferma un container e osserva il ripristino:
docker compose stop api
docker compose ps # api dovrebbe mostrare Exited
docker compose start api
docker compose ps # api dovrebbe mostrare (healthy) dopo lo start_period
docker inspect $(docker compose ps -q api) --format 'RestartCount: {{.RestartCount}}'
Per testare il riavvio automatico su un crash reale, abbassa il limite di memoria sotto il minimo dell'applicazione. La politica di riavvio si attiva quando il processo termina da solo. Nota che docker kill non attiva le politiche di riavvio nelle versioni recenti di Docker.
Riferimento rapido per il dimensionamento delle risorse
Punti di partenza per servizi comuni su un VPS. Regola in base al carico effettivo.
| Servizio | Limite memoria | Limite CPU | Healthcheck |
|---|---|---|---|
| PostgreSQL | 1-4 GB | 1.0-2.0 | pg_isready -U postgres |
| Redis | 256M-1G | 0.25-0.5 | redis-cli ping |
| API Node.js | 256M-1G | 0.5-1.0 | curl -f http://localhost:PORT/health |
| Nginx | 64M-256M | 0.25-0.5 | curl -f http://localhost/ |
| Ollama (LLM) | 4-8 GB | 2.0-4.0 | curl -f http://localhost:11434/ |
| Worker in background | 256M-1G | 0.5-1.0 | Controllo specifico dell'applicazione |
Qualcosa non funziona?
Il container continua a riavviarsi (ciclo di riavvio):
docker compose logs api --tail 50
docker inspect api-1 --format '{{.State.ExitCode}} OOM:{{.State.OOMKilled}} Restarts:{{.RestartCount}}'
Se OOMKilled: true, aumenta il limite di memoria. Se il codice di uscita non è 137, controlla i log dell'applicazione per trovare l'errore reale.
L'healthcheck fallisce sempre:
docker inspect api-1 --format '{{json .State.Health.Log}}' | python3 -m json.tool
Questo mostra l'output di ogni controllo. Cause comuni: l'endpoint di salute non esiste, curl non è installato nell'immagine, o il servizio ascolta su una porta diversa da quella che il controllo punta.
depends_on non aspetta:
Assicurati che la dipendenza abbia un healthcheck definito. Senza, condition: service_healthy non ha nulla da attendere e Compose darà errore all'avvio.
I limiti non appaiono in docker stats:
Verifica di usare Docker Compose v2 (il plugin docker compose, non il vecchio binario docker-compose). Controlla la tua versione:
docker compose version
La chiave deploy.resources richiede Compose v2. Se sei su una versione precedente, consulta per le istruzioni di installazione.
Leggere i log quando qualcosa fallisce:
journalctl -u docker -f
docker compose logs -f --tail 100
Il log del daemon Docker mostra gli eventi OOM e i cambiamenti nel ciclo di vita dei container. I log di Compose mostrano l'output dell'applicazione.
Copyright 2026 Virtua.Cloud. Tutti i diritti riservati. Questo contenuto è un'opera originale del team Virtua.Cloud. La riproduzione, ripubblicazione o redistribuzione senza autorizzazione scritta è vietata.
Pronto a provare?
Distribuisci il tuo server in pochi secondi. Linux, Windows o FreeBSD.
Vedi piani VPS