SSD Nodes Learn Hosting plans →
Guide Matt ConnorDi Matt Connor · Aggiornato 2026-08-27

Limiti di memoria Docker Compose per evitare OOM

Configura deploy.resources e mem_limit in Docker Compose: evita che un container abbatta il VPS, interpreta l'errore exit 137 e gestisci swap e dimensionamento.

Cosa comporta un limite di memoria in Docker Compose

Un limite di memoria in Docker Compose è un tetto massimo rigido imposto dal kernel Linux al cgroup (control group, la funzionalità del kernel che misura le risorse usate da un gruppo 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 prova a superarla, il kernel termina un processo all’interno del container e il container di solito esce con il codice 137.

Questo è particolarmente importante su un VPS, dove la RAM è fissa e non c’è memoria aggiuntiva dell’host da utilizzare. Un container con una perdita di memoria o una query errata può consumare tutte le pagine libere su un sistema con 8GB di RAM. Il kernel termina quindi il processo che considera più problematico. Spesso si tratta di un database o della sessione SSH, non 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. In tal caso, il resto di questa guida non sarà utile finché non risolvi il problema. Se il file Compose è una novità 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 impostazione si applica

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, ovvero del formato che docker compose legge oggi.

Entrambe funzionano su un singolo host. Compose V2, il plugin docker compose, applica deploy.resources.limits e deploy.resources.reservations quando esegui docker compose up, senza alcun cluster Swarm. Le parti del blocco deploy esclusive di Swarm sono le altre chiavi: mode, placement, update_config e endpoint_mode hanno un significato per docker stack deploy e vengono ignorate da docker compose up. È quindi errato sostenere che «deploy richiede Swarm» per la sottosezione resources; seguire questo consiglio lascia i servizi senza alcun limite.

Scegli una sola sintassi per ogni progetto. Scrivere mem_limit: 512m e deploy.resources.limits.memory: 1g nello stesso servizio produce un file difficile da interpretare a colpo d'occhio. Invece di indovinare quale valore sia stato applicato, chiedilo al 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 qualsiasi campo indica che non è stato impostato alcun limite. Il limite di memoria minimo accettato da Docker è 6m; al di sotto di questo valore il container non si avvia.

Cosa accade 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 tutte le risorse possibili all'interno di quel cgroup: prima la page cache pulita, poi le pagine che può spostare nello swap. Se il recupero non libera spazio sufficiente, l'OOM killer (out of memory) 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 signal 9, quindi 137 è l'impronta di qualsiasi SIGKILL, non la prova di un OOM.

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

true 137 indica un OOM kill. false 137 significa che un altro componente ha inviato SIGKILL; la causa usuale è che docker compose stop abbia raggiunto il grace period di dieci secondi perché l'applicazione ha ignorato SIGTERM. Questa distinzione fa risparmiare ore di troubleshooting, 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

Poi leggere il log del kernel, che conserva la registrazione anche dopo un riavvio:

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

Un kill del cgroup stampa una riga che inizia con Memory cgroup out of memory: Killed process 24713 (node). 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 dovrebbero prevenire. La sua presenza indica quindi che la somma dei limiti è troppo alta oppure che alcuni servizi non hanno alcun limite.

Con restart: unless-stopped, un loop di OOM è difficile da individuare, perché il servizio appare attivo in docker compose ps appena un secondo dopo la sua terminazione. Controllare la colonna dell'uptime e il conteggio dei riavvii. Abbinare 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 manualmente.

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

reservations.memory (il precedente mem_reservation) è una soglia minima flessibile. Docker la descrive come un limite flessibile che viene 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 prima memoria dai container che superano la relativa reservation.

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

Contabilità dello swap, senza ambiguità

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

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

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

Considera con realismo i vantaggi dello swap. Rende più lento un OOM kill, ma non lo rende meno probabile, perché un processo che perde memoria riempie lo swap con la stessa facilità con cui riempie la RAM. Nel frattempo, un container che esegue swap thrashing sullo storage VPS condiviso rallenta tutti gli altri servizi presenti sull'host. Per i carichi sensibili alla latenza, un limite corretto senza swap causa un errore più rapidamente e in modo più prevedibile.

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

Il valore MEM USAGE in docker stats include la cache delle pagine. Per questo un container che legge file di grandi dimensioni cresce fino ad avvicinarsi al proprio limite e poi rimane su quel valore. È 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.

Dividere 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 rimosso. file è la cache delle pagine, che invece può essere recuperata. Impostare il limite in base a anon più un margine, non in base al totale. Il file memory.events fornisce una conferma definitiva: 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 nell'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, lascia circa 1GB al kernel, al daemon Docker, a sshd, a journald e alla shell con cui accedi al sistema. Restano circa 7GB da assegnare e la somma dei limiti di tutti i container deve rimanere al di sotto di questo valore. Il sovracommitment funziona fino al giorno in cui due servizi raggiungono insieme il picco di utilizzo.

Una suddivisione pratica su un sistema da 8GB:

  • Reverse proxy: limite di 128m. È un processo di piccole dimensioni e un limite così restrittivo rileva immediatamente un ricaricamento incontrollato della configurazione.
  • PostgreSQL: limite di 2g, con shared_buffers impostato su circa 512MB nella configurazione del database.
  • Container dell’applicazione: limite di 1g.
  • Worker in background: limite di 512m.
  • Servizio multimediale o per i file: limite di 2g, destinato in gran 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à di questo valore come margine. Un limite troppo restrittivo è peggiore dell’assenza di un limite, perché arresta un servizio integro durante un normale picco di traffico.

Un problema merita una nota separata. La maggior parte dei runtime non rileva il limite se non glielo comunichi esplicitamente. PostgreSQL dimensionerà senza problemi shared_buffers e work_mem oltre il limite del container e verrà terminato. Una JVM (Java virtual machine) ha bisogno di -XX:MaxRAMPercentage=75 per dimensionare l’heap in base al limite del cgroup anziché alla RAM dell’host. Node.js richiede --max-old-space-size in megabyte, impostato al di sotto del limite del container; in caso contrario, il garbage collector lascia crescere l’heap finché interviene il kernel. Per Ollama vale lo stesso principio, ma con un parametro diverso, perché aumentare num_ctx fa crescere la cache KV di centinaia di megabyte e il container viene terminato durante l’elaborazione di un prompt lungo. Il cgroup non negozia. Termina il processo.

I limiti CPU si comportano 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 intervallo di 100ms, condiviso da tutti i suoi thread. Quando esaurisce questa quota, il kernel lo mette in attesa fino all'intervallo successivo.

Questa è la differenza importante. Quando supera il limite di memoria, un container viene terminato. Quando supera il limite CPU, viene sottoposto a throttling e continua a funzionare più lentamente. Per questo è sicuro impostare un limite CPU elevato, mentre per la memoria è necessario lasciare margine.

cpu_shares è uno strumento diverso: un peso relativo che conta solo quando le CPU sono effettivamente sature. Due container con shares pari a 1024 e 512 si dividono un core occupato approssimativamente con un rapporto di due a uno; su un host inattivo, invece, nessuno dei due è limitato. Usa shares per stabilire la priorità dei servizi e usa cpus quando ti serve un limite effettivo, ad esempio per impedire che un job notturno di transcodifica monopolizzi le risorse e rallenti il server web.

FAQ

deploy.resources.limits funziona senza Docker Swarm?

Sì. Compose V2 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 contenute in deploy che richiedono effettivamente Swarm sono mode, placement, update_config e endpoint_mode.

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

Significa che il processo principale ha ricevuto SIGKILL, perché 137 è 128 più il segnale 9. La causa più comune è l'OOM killer del kernel, ma un timeout di arresto produce lo stesso codice 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 opzioni funzionano con docker compose. deploy.resources.limits.memory è la sintassi attuale della Compose Specification ed è l'impostazione predefinita consigliata per un nuovo file. Mantieni mem_limit se il resto del file usa già le vecchie chiavi di primo livello. Impostarle entrambe per lo stesso servizio rende soltanto il file più difficile da leggere, quindi scegline una e verifica il risultato con docker inspect.

Perché il mio container utilizza tutto il limite di memoria senza essere terminato?

Il valore di utilizzo in docker stats include la page cache, che il kernel 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 operazioni di input/output su disco, non che sta per terminare.

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

Lascia circa 1GB per il kernel, il demone Docker, sshd, journald e la shell in uso, quindi mantieni la somma dei limiti di tutti i container al di sotto dei 7GB rimanenti. Monitora il valore di picco anon di ogni container sotto carico reale per un giorno prima di fissare i valori e considera il totale un budget, non un obiettivo da saturare.