Out of Memory (OOM) in Docker: Oorzaken, Diagnose en Fixes voor Container Crashes
Out of Memory (OOM) is één van de meest voorkomende oorzaken van “mysterieuze” container-crashes in Docker. Een container draait ogenschijnlijk prima, en ineens stopt hij. Soms zie je alleen Exited (137) of een cryptische melding in logs. In werkelijkheid is er vaak een heel concreet mechanisme aan het werk: de Linux kernel (of Docker via cgroups) grijpt in wanneer geheugen schaars wordt, en beëindigt processen om het systeem te redden.
Deze tutorial legt diepgaand uit waarom OOM gebeurt, hoe je het betrouwbaar diagnosticeert, en welke fixes je kunt toepassen. Alle voorbeelden zijn met echte commando’s (Linux host). Waar relevant noem ik zowel klassieke cgroups v1 als moderne cgroups v2.
Inhoudsopgave
- Wat betekent OOM in Docker precies?
- Hoe Docker geheugen limiteert (cgroups) en wat “memory” echt is
- Typische symptomen: exit codes, logs en gedrag
- Diagnose stap-voor-stap
4.1 Snelle check: container status en OOMKilled
4.2 Kernel logs: dmesg/journalctl (de waarheid)
4.3 Realtime meten: docker stats en top
4.4 Inspecteer cgroup limieten en gebruik (v1/v2)
4.5 Memory leak vs. piekbelasting vs. cache - Oorzaken van OOM in containers
- Fixes en mitigaties
6.1 Geheugenlimieten goed instellen
6.2 Swap: aan/uit en waarom het uitmaakt
6.3 OOM score tuning en prioriteiten
6.4 Applicatie-level fixes (heap, GC, workers, buffers)
6.5 Kubernetes: requests/limits en eviction - Praktische scenario’s en oplossingen
- Checklist voor productie
Wat betekent OOM in Docker precies?
Een Linux-systeem heeft eindig RAM (en eventueel swap). Als processen te veel geheugen claimen en het systeem kan het niet meer leveren, treedt de OOM killer in werking. Die kiest een proces (of meerdere) om te beëindigen zodat het systeem niet volledig vastloopt.
In Docker-context zijn er grofweg twee varianten:
-
Container OOM (cgroup OOM):
De container heeft een geheugenlimiet (bijv. 512MB). De processen in die container overschrijden die limiet. Dan wordt een proces in die cgroup gekilled. Docker rapporteert vaakOOMKilled: trueen de container eindigt met exit code137(SIGKILL). -
Host OOM (system-wide OOM):
Er is géén (of een te hoge) containerlimiet, en de host zelf raakt door geheugen heen. De kernel kiest dan een proces om te killen; dat kan een containerproces zijn, maar ook iets anders (bijv.sshd,dockerd, database op de host). Dit is gevaarlijker omdat het de hele machine instabiel maakt.
Belangrijk: OOM is niet “Docker faalt”, maar een beschermingsmechanisme van de kernel.
Hoe Docker geheugen limiteert (cgroups) en wat “memory” echt is
Docker gebruikt cgroups om resources te beperken. Voor geheugen gaat het om:
- RSS / anonymous memory: heap/stack van je proces (bijv. Java heap, Node buffers, Python objects).
- Page cache: file cache (bijv. gelezen bestanden, layers, logs).
- Kernel memory / slab: kernelstructuren (in moderne setups minder direct gelimiteerd, maar kan wel bijdragen).
- Shared memory (
/dev/shm): belangrijk voor PostgreSQL, Chromium, sommige ML workloads.
cgroups v1 vs v2
- cgroups v1: aparte controllers; memory stats in
/sys/fs/cgroup/memory/... - cgroups v2: unified hierarchy; memory stats in
/sys/fs/cgroup/.../memory.currentetc.
Je kunt zien wat je host gebruikt met:
stat -fc %T /sys/fs/cgroup/
cgroup2fs= v2tmpfs(met losse submounts) = vaak v1
Docker limieten: --memory en --memory-swap
Voorbeeld:
docker run --rm -m 512m --memory-swap 512m nginx:alpine
-m 512mzet de RAM-limiet.--memory-swapbepaalt totale RAM+swap.
Als je--memory-swapgelijk zet aan--memory, is swap voor die container effectief uit.
Zonder limieten kan een container in principe zoveel geheugen pakken als de host toelaat (tot host OOM).
Typische symptomen: exit codes, logs en gedrag
Exit code 137
137 betekent meestal: proces kreeg SIGKILL (9). Bij OOM is dat heel gebruikelijk.
Check:
docker ps -a --no-trunc
Je ziet iets als:
Exited (137) ...
Docker inspect: OOMKilled
docker inspect <container_id> --format '{{.State.OOMKilled}} {{.State.ExitCode}} {{.State.Error}}'
Als OOMKilled true is, dan is het vrijwel zeker cgroup OOM.
Geen logs of abrupt einde
Bij OOM is er vaak geen nette shutdown. Je applicatie krijgt geen kans om logs te flushen. Dat verklaart “ineens weg” zonder stacktrace.
Diagnose stap-voor-stap
Snelle check: container status en OOMKilled
- Vind de container:
docker ps -a --format 'table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Image}}'
- Inspecteer OOM:
CID=<container_id>
docker inspect "$CID" --format 'Name={{.Name}} OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}} FinishedAt={{.State.FinishedAt}}'
- Bekijk events rond het tijdstip:
docker events --since 1h --until 0m
Filter specifiek:
docker events --since 1h | grep -iE 'oom|kill|die|stop'
Kernel logs: dmesg/journalctl (de waarheid)
Bij OOM schrijft de kernel bijna altijd een regel in de kernel log. Op systemd-systemen:
sudo journalctl -k --since "1 hour ago" | grep -i -E 'oom|killed process|out of memory'
Of met dmesg:
sudo dmesg -T | grep -i -E 'oom|killed process|out of memory'
Je ziet vaak iets als:
Memory cgroup out of memory: Killed process 12345 (node) ...Killed process 12345 (java) total-vm:... anon-rss:... file-rss:...
Interpretatie:
- Memory cgroup out of memory = containerlimiet geraakt.
- Out of memory: Kill process … zonder cgroup-context kan host OOM zijn.
Realtime meten: docker stats en top
Voor live inzicht:
docker stats
Dit toont o.a. MEM USAGE / LIMIT. Let op:
- Als de limiet “∞” lijkt of heel hoog is, heb je waarschijnlijk geen limiet gezet.
- Als usage dicht tegen limit aan zit en daarna de container sterft: sterke indicatie.
Processen in container:
docker top <container_id>
Of interactief:
docker exec -it <container_id> sh
# of bash indien beschikbaar
ps aux
free -m
Let op: free in container kan misleidend zijn (ziet soms host memory). Vertrouw vooral op cgroup metrics.
Inspecteer cgroup limieten en gebruik (v1/v2)
cgroups v2 (meest modern)
Vind de cgroup van een container:
CID=<container_id>
PID=$(docker inspect --format '{{.State.Pid}}' "$CID")
cat /proc/$PID/cgroup
Je ziet een pad zoals /docker/<id> of /system.slice/docker-<id>.scope.
Lees limiet en huidig gebruik:
CGPATH=$(cat /proc/$PID/cgroup | awk -F: '{print $3}')
# Op v2 is de mount meestal /sys/fs/cgroup
sudo cat /sys/fs/cgroup${CGPATH}/memory.max
sudo cat /sys/fs/cgroup${CGPATH}/memory.current
sudo cat /sys/fs/cgroup${CGPATH}/memory.events
sudo cat /sys/fs/cgroup${CGPATH}/memory.stat | head -n 50
Belangrijke velden:
memory.max: limiet (maxbetekent geen limiet).memory.current: huidig gebruik.memory.events: counters zoalsoomenoom_kill.memory.stat: breakdown (anon, file, slab, etc.).
cgroups v1
Memory controller pad:
PID=$(docker inspect --format '{{.State.Pid}}' "$CID")
cat /proc/$PID/cgroup | grep memory
Dan:
# voorbeeldpad:
# /docker/<id>
CGPATH=$(cat /proc/$PID/cgroup | awk -F: '/memory/ {print $3}')
sudo cat /sys/fs/cgroup/memory${CGPATH}/memory.limit_in_bytes
sudo cat /sys/fs/cgroup/memory${CGPATH}/memory.usage_in_bytes
sudo cat /sys/fs/cgroup/memory${CGPATH}/memory.failcnt
sudo cat /sys/fs/cgroup/memory${CGPATH}/memory.stat | head -n 50
memory.failcntstijgt wanneer allocaties falen door limiet.memory.statbevatrss,cache, etc.
Memory leak vs. piekbelasting vs. cache
Een cruciale stap is bepalen wat voor soort geheugenprobleem je hebt:
-
Leak (geleidelijk stijgend):
memory.currentof usage stijgt lineair over tijd, ook bij constante load. Uiteindelijk OOM. -
Piek (spike):
Bij specifieke requests of batch jobs schiet memory omhoog en crasht. Tussen pieken is het normaal. -
Cache / file-backed:
file(page cache) groeit. Dit kan legitiem zijn, maar binnen cgroup-limieten kan het alsnog OOM veroorzaken. Soms helpt het om I/O- en cachinggedrag te begrijpen (bijv. grote reads, decompressie, temp files).
Gebruik memory.stat:
- Hoge
anonwijst op heap/objects. - Hoge
filewijst op page cache. - Hoge
slabkan kernel overhead zijn.
Oorzaken van OOM in containers
1) Te lage memory limit (of default in platform)
Je applicatie heeft simpelweg meer nodig dan de ingestelde limiet. Dit gebeurt vaak bij:
- Java services met standaard heap die niet goed afgestemd is op containerlimieten (al is dit sinds Java 10+ veel beter).
- Node.js die grote JSON payloads verwerkt.
- Python data processing.
2) Geen limiet → host OOM
Zonder limieten kan één container de host uitputten. Dan kan de kernel willekeurig iets killen (soms zelfs dockerd), met grote impact.
3) Concurrency en worker-aantallen
Bij webservers: te veel workers/threads, elk met eigen buffers/heaps:
gunicornmet veel workerspuma/unicornnginxmet grote buffers- Java thread stacks
4) Grote payloads / buffering / decompressie
Voorbeelden:
- Reverse proxy die request bodies buffert
- Applicatie die hele bestanden in RAM leest
- (De)serialisatie van grote JSON/XML
- Gzip decompressie in memory
5) /dev/shm te klein (niet OOM maar “crash door shared memory”)
Soms lijkt het op OOM, maar is het eigenlijk een shared memory limiet. Docker zet /dev/shm standaard op 64MB. Chromium, PostgreSQL, sommige ML libs kunnen falen.
Check:
docker exec -it <cid> df -h /dev/shm
Oplossing:
docker run --shm-size=512m ...
6) Memory fragmentation en allocator gedrag
Sommige workloads (bijv. met jemalloc, glibc malloc) kunnen geheugen vasthouden. RSS daalt niet snel na pieken. Dan lijkt het alsof je “leakt”, maar het is allocator caching.
7) Log volume en page cache
Veel logging naar files kan page cache opblazen. In containers met limieten telt page cache mee. Als je logt naar JSON files in een volume, kan file in memory.stat groeien.
Fixes en mitigaties
Geheugenlimieten goed instellen
Doel: container mag niet de host slopen, maar krijgt wel genoeg om stabiel te draaien.
Docker run
docker run -d --name api \
--memory=1g \
--memory-reservation=768m \
--memory-swap=1g \
myorg/api:latest
--memory: harde limiet.--memory-reservation: soft limit (bij druk kan kernel eerder reclaimen).--memory-swap: totale limiet incl. swap.
Docker Compose
In Compose v2 (niet Swarm) zijn sommige deploy-limits niet enforced. Gebruik (waar ondersteund) direct:
docker compose run --rm --memory=1g --memory-swap=1g api
Voor productie is het vaak beter om in Kubernetes of Swarm echte limieten af te dwingen, of Compose met engine die dit ondersteunt. Controleer altijd met:
docker inspect <cid> --format '{{.HostConfig.Memory}} {{.HostConfig.MemorySwap}}'
(waarden in bytes)
Swap: aan/uit en waarom het uitmaakt
Swap kan OOM uitstellen, maar ook latency veroorzaken. In containers is swap vaak expliciet beperkt.
- Swap uit (memory-swap = memory): sneller falen, voorspelbaar, maar sneller OOM.
- Swap aan: minder snel crashen, maar mogelijk trager en kan host swap thrashen.
Voorbeeld: 512MB RAM, 1GB totaal incl swap:
docker run --rm -m 512m --memory-swap 1g myimage
Let op: als de host swap uit heeft staan, kan de container ook niet echt swappen.
Check host swap:
swapon --show
free -h
OOM score tuning en prioriteiten
Linux gebruikt oom_score_adj om te bepalen wat eerder gekilled wordt. In containers wil je meestal:
- Kritieke infra (bijv. database) minder snel gekilled
- Niet-kritieke workers eerder
In Docker kun je --oom-score-adj gebruiken:
docker run -d --oom-score-adj -500 --name db postgres:16
docker run -d --oom-score-adj 200 --name worker myorg/worker:latest
Hoe lager, hoe minder kans om gekilled te worden (range grofweg -1000 tot 1000).
Let op: bij cgroup OOM kan alsnog een proces in dezelfde cgroup worden gekilled; oom_score_adj helpt vooral bij host OOM en bij keuze binnen cgroup, afhankelijk van kernel.
Applicatie-level fixes (heap, GC, workers, buffers)
Dit is vaak de echte oplossing: je app moet binnen het budget passen.
Java (JVM)
Probleem: heap te groot of niet afgestemd op containerlimiet.
Aanpak:
- Zet max heap expliciet of percentage.
- Houd ruimte over voor metaspace, threads, direct buffers, native libs.
Voorbeeld:
JAVA_TOOL_OPTIONS="-XX:MaxRAMPercentage=70 -XX:InitialRAMPercentage=50 -XX:+UseG1GC"
docker run -e JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS" -m 1g myjavaapp
Of expliciet:
JAVA_TOOL_OPTIONS="-Xms512m -Xmx700m"
Diagnose in container:
jcmd 1 VM.flags
jcmd 1 VM.native_memory summary
Node.js
Node heeft een default heap limiet die kan verschillen per versie. In containers wil je vaak --max-old-space-size.
node --max-old-space-size=512 server.js
In Docker:
docker run -e NODE_OPTIONS="--max-old-space-size=512" -m 1g mynodeapp
Python / Gunicorn
Te veel workers is een klassieke OOM trigger.
gunicorn app:app --workers 2 --threads 4 --max-requests 1000 --max-requests-jitter 100
--max-requestshelpt tegen leaks door periodiek worker recycle.
Nginx buffering
Als Nginx grote bodies buffert:
client_body_buffer_sizeproxy_buffer_size,proxy_buffers,proxy_busy_buffers_size
Zet limieten en stream waar mogelijk.
Dataprocessing: streamen i.p.v. in-memory
- Lees files in chunks
- Gebruik iterators/generators
- Vermijd
read()van hele file - Let op decompressie: stream decompressie
Kubernetes: requests/limits en eviction
In Kubernetes zijn OOM’s vaak zichtbaar als OOMKilled in pod status.
Check:
kubectl describe pod <pod>
kubectl get pod <pod> -o jsonpath='{.status.containerStatuses[*].lastState.terminated.reason}'
Stel resources in:
kubectl set resources deployment myapp \
--limits=memory=1Gi \
--requests=memory=512Mi
Belangrijk:
limits.memoryis harde cgroup limiet → OOMKill als overschreden.requests.memorybepaalt scheduling; te laag request kan leiden tot node pressure en eviction.
Node pressure events:
kubectl describe node <node> | grep -i -A3 -E 'MemoryPressure|Eviction'
Praktische scenario’s en oplossingen
Scenario 1: Container exit 137, OOMKilled=true
Symptoom:
docker inspect api --format '{{.State.ExitCode}} {{.State.OOMKilled}}'
# 137 true
Aanpak:
- Check limiet:
docker inspect api --format 'Memory={{.HostConfig.Memory}} MemorySwap={{.HostConfig.MemorySwap}}'
- Check kernel log:
sudo journalctl -k --since "30 min ago" | grep -i oom
- Fix:
- Verhoog
--memoryof verlaag app-gebruik (workers/heap). - Voeg metrics toe (zie checklist).
Scenario 2: Host wordt traag, meerdere containers sterven, OOMKilled=false
Dit kan host OOM zijn.
- Zoek host OOM in kernel log:
sudo journalctl -k --since "2 hours ago" | grep -i -E 'out of memory|killed process'
- Bekijk wie veel gebruikt:
ps aux --sort=-%mem | head -n 20
docker stats --no-stream
- Fix:
- Zet memory limits op alle containers.
- Overweeg swap of meer RAM.
- Zet
--oom-score-adjvoor kritieke services.
Scenario 3: memory.stat toont hoge file (page cache) en OOM
- Inspecteer:
# v2
sudo cat /sys/fs/cgroup/<pad>/memory.stat | egrep 'file |anon |slab '
- Mogelijke oorzaken:
- Grote file reads
- Logging naar file/volume
- Temp files
- Fix:
- Verminder file I/O of buffer sizes
- Log naar stdout/stderr (Docker logging driver) in plaats van grote files in container
- Zorg dat app niet onnodig grote files herhaaldelijk leest
Scenario 4: /dev/shm issue lijkt op OOM
Check:
docker exec -it <cid> df -h /dev/shm
Fix:
docker run --shm-size=1g ...
Checklist voor productie
1) Zet altijd memory limits (en test ze)
- Docker:
--memory,--memory-swap - Kubernetes:
resources.requests/limits
2) Monitor memory op cgroup-niveau
docker statsis oké voor handmatig- Voor productie: Prometheus + cAdvisor / node_exporter of OpenTelemetry metrics
- Belangrijk:
memory.current,memory.max,oom_killcounters
3) Verzamel kernel OOM logs
- Centraliseer
journalctl -koutput (bijv. via fluent-bit) - Alert op
Memory cgroup out of memoryenoom-kill
4) Stel app-limieten in
- JVM heap percentages
- Node
--max-old-space-size - Worker counts (gunicorn/puma/etc.)
- Buffer sizes en streaming
5) Houd overhead in gedachten
Als je container limiet 1Gi is, geef je app niet 1Gi heap. Laat ruimte voor:
- native libs
- thread stacks
- direct buffers
- page cache (soms)
- runtime overhead
6) Reproduceer onder load
Gebruik load tests om piek-geheugen te zien. Bijvoorbeeld met hey:
hey -z 60s -c 50 http://localhost:8080/
En tegelijk:
docker stats api
Extra: Snelle “OOM triage” commands (copy/paste)
Vervang <cid>:
CID=<cid>
docker inspect "$CID" --format 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}} Error={{.State.Error}}'
docker logs --tail 200 "$CID"
sudo journalctl -k --since "2 hours ago" | grep -i -E "oom|killed process|out of memory"
docker stats --no-stream "$CID"
PID=$(docker inspect --format '{{.State.Pid}}' "$CID")
echo "PID=$PID"
cat /proc/$PID/cgroup
Voor cgroups v2:
CGPATH=$(cat /proc/$PID/cgroup | awk -F: '{print $3}')
sudo sh -c "cat /sys/fs/cgroup${CGPATH}/memory.max; cat /sys/fs/cgroup${CGPATH}/memory.current; cat /sys/fs/cgroup${CGPATH}/memory.events"
Samenvatting
OOM in Docker is bijna altijd terug te voeren op één van twee dingen: je container overschrijdt zijn cgroup geheugenlimiet of de host raakt door zijn geheugen heen. De snelste betrouwbare bron is de kernel log (journalctl -k / dmesg), gevolgd door docker inspect (OOMKilled) en cgroup metrics (memory.current, memory.max, memory.events).
De beste structurele aanpak is:
- Stel realistische limieten in,
- stem je applicatie (heap/workers/buffers) af op die limieten,
- monitor cgroup memory en OOM events,
- en voorkom host OOM door overal grenzen te zetten.
Als je wilt, kun je je docker run/Compose/Kubernetes config en de output van docker inspect ..., journalctl -k ... en (v2) memory.events delen; dan kan ik gericht helpen bepalen of het om anon/file/slab gaat en welke instelling het meest effectief is.