Limiti di risorse, healthcheck e politiche di riavvio in Docker Compose

13 min di lettura·Matthieu·vpsproductioncontainersdocker-composedocker|

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