Limiti memoria Docker Compose per evitare OOM
Imposta limiti RAM e CPU in Docker Compose: usa deploy.resources o mem_limit, riconosci l'uscita 137 e configura swap per proteggere il tuo VPS.
Cosa comporta un limite di memoria Docker Compose
Un limite di memoria Docker Compose è un limite rigido che il kernel Linux applica al cgroup (gruppo di controllo, 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 termina con il codice 137.
Questo è particolarmente importante su un VPS, dove la RAM è fissa e non c'è memoria host disponibile aggiuntiva da utilizzare. Un container con una perdita di memoria o una query errata può occupare ogni pagina libera su un sistema da 8GB. 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 problema relativo a un solo servizio, che può essere riavviato.
services:
app:
image: ghcr.io/example/app:1.4
deploy:
resources:
limits:
cpus: "1.5"
memory: 1g
reservations:
memory: 256mApplicalo e verifica che il limite sia attivo:
docker compose up -d
docker stats --no-streamLa 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 lo sarà. Se il file Compose è una novità per te, le basi di Docker Compose per un VPS illustrano la struttura del file su cui si basa questa configurazione.
deploy.resources.limits o mem_limit: quale 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, il 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 specifiche di Swarm del blocco deploy 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. Pertanto, il consiglio comune secondo cui "deploy richiede Swarm" è errato per la sottosezione resources. Se lo segui, i tuoi servizi rimangono completamente privi di limiti.
Scegli una sola sintassi per progetto. Scrivere mem_limit: 512m e deploy.resources.limits.memory: 1g nello stesso servizio produce un file di cui non è possibile capire il comportamento a colpo d'occhio. Invece di indovinare quale valore abbia prevalso, chiedilo al daemon:
docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}} {{.HostConfig.NanoCpus}}' app-1I 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 è già al suo memory.max, il kernel recupera prima le risorse possibili all'interno di quel cgroup: la cache delle pagine pulite, quindi le pagine che può spostare nello swap. Se il recupero non libera spazio sufficiente, il killer OOM (out of memory) del cgroup seleziona un processo all'interno del container e gli invia SIGKILL. L'eventuale terminazione 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 non dimostra da solo che si sia verificato un OOM.
docker compose ps -a
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app-1true 137 indica una terminazione OOM. false 137 indica che SIGKILL è stato inviato da un'altra origine; la causa più comune è docker compose stop che raggiunge il periodo di tolleranza di dieci secondi perché l'applicazione ha ignorato SIGTERM. Questa distinzione fa risparmiare ore, 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=oomPoi 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'Una terminazione del cgroup stampa una riga che inizia con Memory cgroup out of memory: Killed process 24713 (node). Una riga priva del prefisso Memory cgroup indica un OOM dell'host, ovvero che la macchina stessa ha esaurito la RAM. È proprio il problema che i limiti dovrebbero impedire. Se compare, significa che la somma dei limiti è troppo alta oppure che alcuni servizi non hanno alcun limite.
Con restart: unless-stopped, un ciclo OOM è difficile da rilevare, perché il servizio risulta attivo in docker compose ps appena un secondo dopo la 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 termina continuamente resta visibile senza doverlo monitorare.
La reservation è un'indicazione, il limite è la regola
reservations.memory (la 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 semplicemente al kernel di recuperare prima la memoria dai container che superano la relativa reservation.
Quindi, una reservation non protegge nulla da sola. Usala per indicare un servizio che vuoi venga trattato con priorità ridotta 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 distribuita 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 alcun effetto e il limite di memoria corrisponde esclusivamente alla RAM.
memswap_limit non indica la quantità di swap. Indica il totale della memoria più lo 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 affatto di swap. Impostando mem_limit e lasciando memswap_limit non configurato, il container può usare una quantità di swap pari nuovamente alla dimensione del proprio limite di memoria.
Ubuntu 24.04 e Debian 13 usano cgroup v2 per impostazione predefinita, dove lo swap è un contatore separato (memory.swap.max) e questa configurazione funziona senza ulteriori impostazioni. Il vecchio messaggio Your kernel does not support swap limit capabilities proviene dagli 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.
È importante valutare correttamente il vantaggio offerto dallo swap. Rende più lento un OOM kill, ma non ne riduce la probabilità, perché un processo che perde memoria riempie lo swap con la stessa facilità con cui riempie la RAM. Nel frattempo, un container che usa intensivamente lo swap su storage VPS condiviso rallenta tutti gli altri servizi del server. Per qualsiasi carico sensibile alla latenza, un limite corretto senza swap causa il fallimento 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, quindi un container che legge file di grandi dimensioni cresce fino ad avvicinarsi al proprio limite e vi rimane. È normale e non indica una perdita di memoria, perché la cache pulita viene recuperata prima che venga mai chiamato l'OOM killer. Un servizio come un server multimediale Jellyfin autogestito sembrerà permanentemente vicino al proprio limite proprio per questo motivo.
Separare la cache dal 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.eventsanon è la memoria anonima, il working set che non può essere rilasciato. file è la cache delle pagine, che può essere rilasciata. Impostare il limite in base a anon più un margine, non in base al totale. Il file memory.events risolve definitivamente il dubbio: un contatore oom_kill superiore a zero indica che il kernel ha terminato un processo 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
Partire dall'host, non dalle applicazioni. Su un VPS da 8GB, lasciare circa 1GB per il kernel, il demone Docker, sshd, journald e la propria shell di accesso. Restano circa 7GB da distribuire, e la somma dei limiti di tutti i container deve rimanere al di sotto di questo valore. Il sovraccarico funziona fino al giorno in cui due servizi raggiungono insieme il picco di utilizzo.
Una suddivisione praticabile su un sistema da 8GB:
- Reverse proxy: limite di 128m. È un processo di piccole dimensioni e un limite così stretto rileva immediatamente un ricaricamento incontrollato della configurazione.
- PostgreSQL: limite di 2g, con
shared_buffersimpostato su circa 512MB nella configurazione del database. - Container dell'applicazione: limite di 1g.
- Worker in background: limite di 512m.
- Servizio per contenuti multimediali o file: limite di 2g, destinati per la maggior parte alla page cache.
Non copiare questi valori nel proprio stack. Eseguire i servizi sotto carico reale per un giorno, monitorare docker stats, rilevare il valore di picco anon per ogni container e aggiungere circa la metà come margine. Un limite impostato in modo troppo restrittivo è peggiore dell'assenza di un limite, perché interrompe un servizio integro durante un normale picco del traffico.
Un problema merita una nota separata. Il limite è invisibile alla maggior parte dei runtime, a meno che non venga comunicato loro. 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, invece che alla RAM dell'host. Node.js ha bisogno di --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. Il cgroup non negozia. Termina il processo.
I limiti della 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, condivisi tra tutti i thread. Quando esaurisce questo tempo, il kernel lo mette in attesa fino all'intervallo successivo.
Questa è la differenza importante. Un container che supera il limite di memoria viene terminato. Un container che supera il limite della CPU viene sottoposto a throttling e continua a funzionare più lentamente. Per questo è sicuro impostare un limite CPU aggressivo, mentre un limite di memoria richiede 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 con un rapporto approssimativo 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 processo di transcodifica notturno saturi le risorse del 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 all'interno di 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 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'uscita 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 chiavi di primo livello precedenti. Impostarle entrambe per lo stesso servizio rende il file solo più difficile da leggere. Scegline una 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 la cache delle pagine, che il kernel elimina quando la memoria è sotto pressione invece di attivare un'uscita per esaurimento della memoria. 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 alto 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 daemon Docker, sshd, journald e la tua shell. Mantieni quindi la somma dei limiti di tutti i container al di sotto dei 7GB rimanenti. Durante una giornata di carico reale, controlla il valore di picco anon per ogni container prima di definire i valori. Considera il totale un budget, non un obiettivo da saturare.