← Terug naar tutorials

Out of Memory (OOM) in Docker: Oorzaken, Diagnose en Fixes voor Container Crashes

dockeroomcontainersdevopskubernetescgroupsmemory-limitstroubleshootingmonitoringperformance

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

  1. Wat betekent OOM in Docker precies?
  2. Hoe Docker geheugen limiteert (cgroups) en wat “memory” echt is
  3. Typische symptomen: exit codes, logs en gedrag
  4. 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
  5. Oorzaken van OOM in containers
  6. 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
  7. Praktische scenario’s en oplossingen
  8. 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:

  1. 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 vaak OOMKilled: true en de container eindigt met exit code 137 (SIGKILL).

  2. 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:

cgroups v1 vs v2

Je kunt zien wat je host gebruikt met:

stat -fc %T /sys/fs/cgroup/

Docker limieten: --memory en --memory-swap

Voorbeeld:

docker run --rm -m 512m --memory-swap 512m nginx:alpine

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:

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

  1. Vind de container:
docker ps -a --format 'table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Image}}'
  1. Inspecteer OOM:
CID=<container_id>
docker inspect "$CID" --format 'Name={{.Name}} OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}} FinishedAt={{.State.FinishedAt}}'
  1. 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:

Interpretatie:

Realtime meten: docker stats en top

Voor live inzicht:

docker stats

Dit toont o.a. MEM USAGE / LIMIT. Let op:

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:

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 leak vs. piekbelasting vs. cache

Een cruciale stap is bepalen wat voor soort geheugenprobleem je hebt:

  1. Leak (geleidelijk stijgend):
    memory.current of usage stijgt lineair over tijd, ook bij constante load. Uiteindelijk OOM.

  2. Piek (spike):
    Bij specifieke requests of batch jobs schiet memory omhoog en crasht. Tussen pieken is het normaal.

  3. 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:


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:

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:

4) Grote payloads / buffering / decompressie

Voorbeelden:

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

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.

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:

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:

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

Nginx buffering

Als Nginx grote bodies buffert:

Zet limieten en stream waar mogelijk.

Dataprocessing: streamen i.p.v. in-memory

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:

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:

  1. Check limiet:
docker inspect api --format 'Memory={{.HostConfig.Memory}} MemorySwap={{.HostConfig.MemorySwap}}'
  1. Check kernel log:
sudo journalctl -k --since "30 min ago" | grep -i oom
  1. Fix:

Scenario 2: Host wordt traag, meerdere containers sterven, OOMKilled=false

Dit kan host OOM zijn.

  1. Zoek host OOM in kernel log:
sudo journalctl -k --since "2 hours ago" | grep -i -E 'out of memory|killed process'
  1. Bekijk wie veel gebruikt:
ps aux --sort=-%mem | head -n 20
docker stats --no-stream
  1. Fix:

Scenario 3: memory.stat toont hoge file (page cache) en OOM

  1. Inspecteer:
# v2
sudo cat /sys/fs/cgroup/<pad>/memory.stat | egrep 'file |anon |slab '
  1. Mogelijke oorzaken:
  1. Fix:

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)

2) Monitor memory op cgroup-niveau

3) Verzamel kernel OOM logs

4) Stel app-limieten in

5) Houd overhead in gedachten

Als je container limiet 1Gi is, geef je app niet 1Gi heap. Laat ruimte voor:

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:

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.