SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-10-13

Limiti di memoria Docker Compose contro l’errore OOM

Imposta limiti di RAM e CPU in Docker Compose per proteggere il VPS: usa deploy.resources o mem_limit, interpreta l’uscita 137 e gestisci swap e sizing.

Che cosa determina un limite di memoria in Docker Compose

Un limite di memoria in Docker Compose è un tetto massimo rigido che il kernel Linux applica al cgroup (control group, la funzionalità del kernel che misura le risorse per un insieme di processi) di un container. Imposta deploy.resources.limits.memory su un servizio e quel container non potrà mai usare più della quantità specificata. Quando tenta di superarla, il kernel termina un processo all'interno del container e il container generalmente esce con il codice 137.

Questo è particolarmente importante su un VPS, dove la RAM è fissa e non è disponibile memoria aggiuntiva dell'host. Un container con una perdita di memoria o una query errata può consumare ogni pagina libera su un sistema con 8GB di RAM. Il kernel termina quindi il processo che considera più problematico, spesso un database o la sessione SSH, invece del container che ha causato il problema. I limiti trasformano un'interruzione dell'intero server in un singolo servizio che viene riavviato.

services:
  app:
    image: ghcr.io/example/app:1.4
    deploy:
      resources:
        limits:
          cpus: "1.5"
          memory: 1g
        reservations:
          memory: 256m

Applicalo e verifica che il limite sia attivo:

docker compose up -d
docker stats --no-stream

La colonna MEM USAGE / LIMIT dovrebbe contenere un valore simile a 142MiB / 1GiB. Se la colonna del limite mostra l'intera RAM dell'host, l'impostazione non è stata applicata e il resto di questa guida non sarà utile finché non la correggi. Se il file Compose è nuovo per te, le basi di Docker Compose per un VPS descrivono la struttura del file su cui si basa questa configurazione.

deploy.resources.limits o mem_limit: quale viene applicato

Esistono due sintassi per lo stesso concetto, ed è questo che crea confusione.

mem_limit, mem_reservation, memswap_limit, cpus e cpu_shares sono chiavi di servizio di primo livello ereditate dai formati precedenti dei file Compose. deploy.resources proviene dallo schema Swarm e ora fa parte della Compose Specification, il formato che docker compose legge oggi.

Entrambe funzionano su un singolo host. Docker Compose, il plugin docker compose, applica deploy.resources.limits e deploy.resources.reservations quando si esegue docker compose up, senza alcun cluster Swarm. Il plugin è ora nella serie v5 (v5.6.0 era l'ultima release stabile al momento della verifica, il 2026-10-10). È passato direttamente dalla v2 alla v5, saltando la 3 e la 4 per evitare confusione con le vecchie versioni dei formati dei file Compose. Nessuna release v5 ha modificato la gestione dei limiti delle risorse. Le parti del blocco deploy orientate a Swarm sono le altre chiavi, ad esempio placement, update_config e endpoint_mode, che descrivono la pianificazione e il rilascio degli aggiornamenti su un cluster di nodi per docker stack deploy. Pertanto, l'affermazione comune secondo cui "deploy richiede Swarm" è errata per la sottosezione delle risorse. Seguirla lascia i servizi senza alcun limite.

Scegli una sola sintassi per ogni progetto. Scrivere mem_limit: 512m e deploy.resources.limits.memory: 1g sullo stesso servizio non è soltanto difficile da leggere: la Compose Specification stabilisce che, quando è impostato mem_limit, deve essere coerente con limits.memory, ma non specifica quale valore prevalga in caso di conflitto. Qualunque sintassi si utilizzi, verificare quale configurazione ha effettivamente applicato il demone:

docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}} {{.HostConfig.NanoCpus}}' app-1

I valori della memoria sono espressi in byte, quindi 1g restituisce 1073741824. La CPU è espressa in nano CPU, quindi 1.5 restituisce 1500000000. Un 0 in un campo indica che non è stato impostato alcun limite. Il limite minimo di memoria accettato da Docker è 6m; al di sotto di questo valore, il container non si avvia.

Cosa succede quando un container raggiunge il limite

Il container non rallenta. Termina.

Quando un processo richiede una pagina e il cgroup ha già raggiunto il proprio memory.max, il kernel recupera prima tutto ciò che può all'interno di quel cgroup: la page cache pulita, quindi le pagine che può spostare nello swap. Se il recupero non libera spazio sufficiente, l'OOM killer del cgroup seleziona un processo all'interno del container e gli invia SIGKILL. L'uccisione del PID 1 del container termina il container. Il codice di uscita 137 è semplicemente 128 più il segnale 9, quindi 137 è l'impronta di qualsiasi SIGKILL, ma da solo non dimostra che si sia verificato un OOM.

docker compose ps -a
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app-1

true 137 indica un'uccisione per OOM. false 137 di solito indica che qualcos'altro ha inviato SIGKILL (un programma può anche terminare volontariamente con 137, ma è raro); la causa abituale è che docker compose stop ha raggiunto il periodo di tolleranza di dieci secondi perché l'applicazione ha ignorato SIGTERM. Questa distinzione fa risparmiare ore di analisi, perché i due problemi non hanno nulla in comune.

L'evento viene registrato anche in altri due punti. Monitorare il daemon in tempo reale:

docker events --filter event=oom

Quindi leggere il log del kernel, che conserva il record anche dopo un riavvio:

sudo dmesg -T | grep -i -E 'memory cgroup out of memory|killed process'

Un'uccisione eseguita da un cgroup stampa una riga che inizia con Memory cgroup out of memory: Killed process 24713 (node). Questa riga indica che un container ha raggiunto il proprio limite e può comparire anche quando sull'host è ancora disponibile molta RAM. Una riga senza il prefisso Memory cgroup indica un OOM dell'host, cioè che la macchina ha esaurito la RAM. È proprio il problema che i limiti devono prevenire. Se compare, di solito significa che la somma dei limiti è troppo alta, che alcuni servizi non hanno alcun limite oppure che qualcosa al di fuori di Docker sta utilizzando più risorse del margine lasciato.

Con restart: unless-stopped, un ciclo di OOM può passare inosservato, perché un secondo dopo il crash il servizio risulta nuovamente attivo in docker compose ps. Controllare la colonna dell'uptime e il numero di riavvii. Associare inoltre il limite a un healthcheck che segnala l'applicazione come non integra, così un container che continua a terminare resta visibile senza doverlo monitorare continuamente.

La reservation è un’indicazione; il limite è la regola

reservations.memory (la precedente mem_reservation) è una soglia minima non vincolante. Docker la descrive come un limite flessibile, attivato quando il daemon rileva contesa o poca memoria sull’host. Non impedisce mai a un container di superarla e non garantisce mai che la memoria sia disponibile quando il container la richiede. Indica soltanto al kernel di recuperare memoria prima dai container che superano la relativa reservation.

Una reservation, quindi, non protegge nulla da sola. Usala per indicare un servizio che vuoi favorire in condizioni di pressione e affidati al limite per la sicurezza. Mantieni la reservation al di sotto del limite, altrimenti il container non si avvia: Docker rifiuta la configurazione con Minimum memory limit can not be less than memory reservation limit.

Gestione della swap, senza ambiguità

La maggior parte delle immagini VPS viene distribuita senza alcun file di swap. Esegui swapon --show e free -h. Se il totale della swap è zero, tutte le impostazioni relative alla swap riportate di seguito non hanno effetto e il limite di memoria è un limite esclusivo della RAM.

memswap_limit non indica la quantità di swap. Indica il totale della memoria più la swap. Con mem_limit: 1g e memswap_limit: 2g, il container dispone di 1GB di RAM e 1GB di swap. Impostando i due valori allo stesso modo, il container non dispone di swap. Impostando mem_limit e lasciando memswap_limit non impostato, il container può usare la swap fino alla dimensione del proprio limite di memoria.

Ubuntu 24.04 e Debian 13 usano cgroup v2 per impostazione predefinita, dove la swap è un contatore separato (memory.swap.max) e questa configurazione funziona senza ulteriori operazioni. Il vecchio messaggio Your kernel does not support swap limit capabilities viene visualizzato sugli host con cgroup v1 avviati senza swapaccount=1. Su questi host il limite di memoria continua ad applicarsi, mentre la parte relativa alla swap viene ignorata.

È importante valutare correttamente i vantaggi della swap. Rende più lento un OOM kill, ma non ne riduce la probabilità, perché un processo con una perdita di memoria riempie la swap con la stessa facilità con cui riempie la RAM. Nel frattempo, un container che usa intensivamente la swap sullo storage VPS condiviso rallenta tutti gli altri servizi del server. Per i servizi sensibili alla latenza, un limite corretto senza swap consente di terminare più rapidamente e in modo più prevedibile.

Perché l'utilizzo della memoria sembra peggiore di quanto sia

In Linux, la figura MEM USAGE in docker stats sottrae soltanto la cache delle pagine inattive (inactive_file nei cgroup v2). La cache usata di recente continua quindi a essere conteggiata. Un container che legge file di grandi dimensioni aumenta l'utilizzo fino ad avvicinarsi al proprio limite e poi rimane a quel livello. È un comportamento normale e non indica una perdita di memoria, perché la cache pulita viene recuperata prima che venga chiamato l'OOM killer. Un servizio come un server multimediale Jellyfin self-hosted apparirà stabilmente vicino al limite proprio per questo motivo.

Suddividi il valore tra cache e working set effettivo dall'interno del container:

docker compose exec app grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat
docker compose exec app cat /sys/fs/cgroup/memory.events

anon è la memoria anonima, cioè il working set che non può essere eliminato. file è la cache delle pagine, che invece può essere recuperata. Imposta il limite in base a anon più un margine, non sul totale. Il file memory.events risolve definitivamente il dubbio: un contatore oom_kill superiore a zero indica che il kernel ha terminato qualcosa in questo container dall'avvio, mentre un contatore max in aumento indica che il container sta raggiungendo il proprio limite in questo momento. Entrambi i comandi richiedono una shell e coreutils all'interno dell'immagine, quindi non funzionano con un'immagine distroless o scratch.

Limiti di dimensionamento su un VPS da 8GB

Parti dall’host, non dalle applicazioni. Su un VPS da 8GB, riserva circa 1GB al kernel, al demone Docker, a sshd, a journald e alla tua shell di login. Restano all’incirca 7GB da assegnare, quindi la somma dei limiti di tutti i container deve restare al di sotto di questo valore. L’overcommit funziona fino al giorno in cui due servizi raggiungono contemporaneamente il picco.

Una suddivisione praticabile su un sistema da 8GB:

  • Reverse proxy: limite 128m. È un processo di piccole dimensioni e un limite così restrittivo rileva immediatamente un ricaricamento incontrollato della configurazione.
  • PostgreSQL: limite 2g, con shared_buffers impostato a circa 512MB nella configurazione del database.
  • Container dell’applicazione: limite 1g.
  • Worker in background: limite 512m.
  • Servizio multimediale o di file: limite 2g, destinato per la maggior parte alla page cache.

Non copiare questi valori nel tuo stack. Esegui i servizi sotto carico reale per un giorno, monitora docker stats, rileva il valore di picco anon per ogni container e aggiungi circa la metà come margine. Un limite impostato in modo eccessivamente restrittivo è peggiore dell’assenza di un limite, perché arresta un servizio sano durante un normale picco di traffico.

Una situazione comune merita una nota separata. Alcuni runtime ignorano il limite finché non li informi esplicitamente, mentre quelli che lo rilevano potrebbero non dimensionarsi nel modo previsto. PostgreSQL può impostare shared_buffers e work_mem oltre il limite del container e venire terminato. Una JVM moderna (Java virtual machine) rileva il limite del cgroup per impostazione predefinita, ma le build HotSpot come OpenJDK assegnano all’heap soltanto il 25 percento del limite. Per questo -XX:MaxRAMPercentage=75 è il metodo abituale per consentire all’heap di usare una quota maggiore del limite, lasciando comunque spazio alla memoria esterna all’heap. Node.js calcola la dimensione dell’heap predefinito dal limite del cgroup dalla versione 12, usando circa la metà del limite del container e stabilizzandosi intorno a 2GB. Imposta quindi --max-old-space-size in megabyte soltanto quando vuoi un limite massimo diverso e mantienilo al di sotto del limite del container, così il garbage collector può intervenire prima del kernel. Per Ollama vale lo stesso principio, ma con un parametro diverso: aumentare num_ctx fa crescere la cache KV di centinaia di megabyte e il container viene terminato durante un prompt lungo. Il cgroup non negozia. Termina il processo.

I limiti della CPU funzionano in modo completamente diverso

cpus: "1.5" indica il 150% di un core, applicato come quota CFS (completely fair scheduler). Il container riceve 150ms di tempo CPU in ogni periodo di 100ms, condiviso tra tutti i thread. Quando esaurisce questa quota, il kernel lo sospende fino al periodo successivo.

Questa è la differenza principale. Un container che supera il limite di memoria viene terminato. Un container che supera il limite CPU viene sottoposto a throttling e continua a funzionare più lentamente. Per questo è possibile impostare un limite CPU in modo aggressivo, mentre il limite di memoria deve prevedere un margine.

cpu_shares è uno strumento diverso: un peso relativo che conta soltanto quando le CPU sono effettivamente sature. Due container con shares pari a 1024 e 512 si dividono un core sotto carico approssimativamente con un rapporto di due a uno; su un host inattivo, invece, nessuno dei due viene limitato. Usate shares per stabilire la priorità dei servizi e cpus quando serve un limite massimo effettivo, ad esempio per impedire che un'attività di transcodifica notturna saturi le risorse a scapito del server web.

FAQ

docker compose funziona senza Docker Swarm?

Sì. Docker Compose, il plugin docker compose (ora alla versione v5), applica deploy.resources.limits e deploy.resources.reservations quando esegui docker compose up su un singolo host. Verificalo con docker inspect --format '{{.HostConfig.Memory}}' <container>, che stampa il limite in byte e stampa 0 quando non è stato applicato alcun limite. Le chiavi dentro deploy concepite per Swarm sono quelle relative al cluster, ad esempio placement, update_config e endpoint_mode.

Che cosa significa il codice di uscita 137 in Docker Compose?

Quasi sempre significa che il processo principale ha ricevuto SIGKILL, perché 137 equivale a 128 più il segnale 9. Un programma può anche terminare deliberatamente con 137, ma è raro. La causa più comune è l'OOM killer del kernel, ma lo stesso codice viene prodotto da un timeout di arresto quando un'applicazione ignora SIGTERM. Esegui docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' <container> per distinguerli. true 137 indica un'uccisione per esaurimento della memoria, mentre false 137 no.

Devo impostare mem_limit o deploy.resources.limits.memory?

Entrambe le impostazioni funzionano con docker compose. deploy.resources.limits.memory è la forma attuale della Compose Specification ed è la scelta predefinita migliore per un file nuovo. Mantieni mem_limit se il resto del file usa già le chiavi di primo livello precedenti. La Compose Specification richiede che i due valori siano coerenti quando sono impostati entrambi. Scegline uno e verifica il risultato con docker inspect.

Perché il mio container raggiunge il limite massimo di memoria senza essere terminato?

Il valore di utilizzo in docker stats include ancora la page cache usata di recente. Il kernel la rilascia sotto pressione invece di attivare un'uccisione OOM. Esegui docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat e leggi il valore anon, che rappresenta il working set non recuperabile. Un valore file elevato accanto a un valore anon basso indica che il container sta eseguendo input e output su disco, non che stia per terminare.

Quanta RAM devo lasciare non allocata su un VPS da 8GB?

Lascia circa 1GB per il kernel, il demone Docker, sshd, journald e la shell in uso. Mantieni quindi la somma dei limiti di tutti i container al di sotto dei 7GB rimanenti. Durante una giornata di carico reale, monitora per ogni container il valore di picco anon prima di definire i valori definitivi. Considera il totale un budget, non un obiettivo da saturare.

Questo articolo ti è stato utile?