Docker Out of Memory (OOM) : diagnostiquer et corriger les crashs mémoire des conteneurs
Les crashs OOM (Out Of Memory) dans Docker sont parmi les pannes les plus frustrantes : le conteneur disparaît, redémarre en boucle, ou l’application se fait tuer sans logs explicites. En réalité, il y a presque toujours des indices exploitables : événements du kernel, métriques cgroup, paramètres de limites mémoire, comportements d’allocation (heap, cache, mmap), et parfois une mauvaise interprétation de ce que “mémoire” signifie dans un conteneur.
Ce tutoriel explique comment diagnostiquer précisément un OOM, identifier la cause (fuite mémoire, limite trop basse, cache, swap, page cache, mmap, etc.) et corriger durablement.
1) Comprendre ce qu’est un OOM en environnement Docker
1.1 OOM côté conteneur vs OOM côté hôte
Il existe deux situations principales :
-
OOM “dans” le conteneur (cgroup OOM)
Le conteneur a une limite mémoire (--memory) et dépasse cette limite. Le kernel tue un ou plusieurs processus dans ce cgroup. Docker peut ensuite redémarrer le conteneur selon la policy. -
OOM au niveau de l’hôte (system OOM)
L’hôte n’a plus de mémoire disponible (ou est sous pression extrême), et le kernel déclenche l’OOM killer global. Il peut tuer un processus appartenant à un conteneur (ou pas). Dans ce cas, même sans limite Docker, un conteneur peut être victime.
Conséquence : la stratégie de diagnostic n’est pas la même. Il faut déterminer si l’OOM est cgroup (limite conteneur) ou global (hôte).
2) Symptômes typiques et premières vérifications
2.1 Vérifier l’état du conteneur
docker ps -a --no-trunc
Cherchez :
Exited (137): souvent lié à un SIGKILL, fréquemment OOM.Exited (139): segfault (pas OOM, mais peut être corrélé).- Redémarrages répétés :
Restarting (137).
Ensuite :
docker inspect <container_id_or_name> --format '{{json .State}}' | jq
Points clés :
.OOMKilled:truesi Docker a détecté un OOM kill dans le conteneur..ExitCode:137est un indicateur fréquent.
Exemple :
docker inspect myapp --format 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}} Error={{.State.Error}}'
2.2 Lire les logs applicatifs
docker logs --tail=200 myapp
Attention : si le processus est tué brutalement (SIGKILL), il peut ne rien écrire juste avant la mort. Il faut alors se tourner vers les logs kernel/cgroup.
3) Confirmer l’OOM via les logs du kernel
3.1 Sur l’hôte : dmesg et journalctl
Sur beaucoup de distributions :
sudo dmesg -T | egrep -i 'oom|out of memory|killed process'
Ou avec systemd :
sudo journalctl -k --since "1 hour ago" | egrep -i 'oom|out of memory|killed process'
Vous cherchez des lignes du type :
Out of memory: Killed process 12345 (java) ...Memory cgroup out of memory: Kill process ...oom-kill:constraint=CONSTRAINT_MEMCG ...
Interprétation rapide :
- Si vous voyez
constraint=CONSTRAINT_MEMCGouMemory cgroup out of memory, c’est un OOM lié à une limite cgroup (donc potentiellement la limite Docker). - Si c’est un OOM “global” sans mention de memcg, l’hôte manque de RAM (ou swap) et tue.
4) Vérifier les limites mémoire Docker et les paramètres runtime
4.1 Inspecter la configuration mémoire du conteneur
docker inspect myapp --format '{{json .HostConfig}}' | jq '.Memory, .MemorySwap, .MemoryReservation, .OomKillDisable, .PidsLimit'
Rappels :
.Memory: limite RAM (en octets).0= pas de limite explicite..MemorySwap: limite RAM+swap. Selon versions/config, peut être déroutant..MemoryReservation: “soft limit” (pression mémoire)..OomKillDisable: désactiver l’OOM killer (souvent une mauvaise idée)..PidsLimit: limite de processus (pas mémoire, mais utile en diagnostic).
4.2 Voir les stats à chaud
docker stats myapp
Cela donne une vue globale, mais pas assez fine (cache vs RSS, etc.). Pour aller plus loin, on lit les métriques cgroup.
5) Diagnostic précis via cgroups (v1 et v2)
Docker s’appuie sur les cgroups Linux. Selon votre distribution, vous êtes en cgroup v1 ou cgroup v2.
5.1 Déterminer la version cgroup
stat -fc %T /sys/fs/cgroup
cgroup2fs=> cgroup v2- autre (souvent
tmpfs) => cgroup v1 (ou hybride)
Vous pouvez aussi regarder :
mount | grep cgroup
5.2 Trouver le chemin cgroup d’un conteneur
Récupérer l’ID complet :
CID=$(docker inspect -f '{{.Id}}' myapp)
echo "$CID"
Ensuite, selon le système, le chemin peut varier (systemd, cgroupfs). Vous pouvez chercher :
sudo find /sys/fs/cgroup -name "*$CID*" 2>/dev/null | head
5.3 cgroup v2 : lire memory.current, memory.max, memory.events
Une fois le dossier trouvé (exemple fictif) :
/sys/fs/cgroup/system.slice/docker-$CID.scope/
Commandes :
CG="/sys/fs/cgroup/system.slice/docker-$CID.scope"
cat $CG/memory.max
cat $CG/memory.current
cat $CG/memory.high
cat $CG/memory.swap.max 2>/dev/null || true
cat $CG/memory.events
cat $CG/memory.stat | head -n 50
Ce que vous cherchez :
memory.max: limite dure.maxsignifie illimité.memory.current: consommation actuelle.memory.events: compteurs essentiels, par ex. :oom: nombre d’événements OOM dans ce cgroupoom_kill: nombre de kills effectuéshigh: dépassements dememory.high(throttling mémoire)
Exemple d’interprétation :
oom_kill 1=> le kernel a tué un processus dans ce cgroup.memory.currentproche dememory.max=> limite trop basse ou fuite.
Dans memory.stat, regardez notamment :
anon: mémoire anonyme (heap, stacks) => souvent la “vraie” RAM de l’appfile: page cache (fichiers)slab: caches kernelinactive_file/active_file: page cacheanon_thp: huge pages transparentes (peut gonfler)
5.4 cgroup v1 : lire memory.limit_in_bytes, memory.usage_in_bytes, memory.stat, oom_control
Chemins typiques :
/sys/fs/cgroup/memory/docker/<CID>/ ou via systemd.
Commandes :
CG=$(sudo find /sys/fs/cgroup/memory -name "$CID*" -type d 2>/dev/null | head -n 1)
echo "$CG"
cat $CG/memory.limit_in_bytes
cat $CG/memory.usage_in_bytes
cat $CG/memory.max_usage_in_bytes
cat $CG/memory.failcnt
cat $CG/oom_control
head -n 50 $CG/memory.stat
Indicateurs :
memory.failcntaugmente quand la limite est atteinte.oom_controlpeut indiquer si l’OOM killer est activé.memory.statdonnerss,cache,mapped_file, etc.
6) Identifier la “vraie” cause : fuite, cache, mmap, ou limite mal calibrée
Un OOM ne signifie pas toujours “fuite mémoire”. Voici les causes les plus fréquentes.
6.1 Limite mémoire trop basse (ou mal comprise)
Vous avez défini --memory=512m, mais votre application a besoin de 1–2 Go au pic (warmup, compilation JIT, chargement de modèles ML, indexation, etc.).
Vérification :
memory.currentproche dememory.maxoom_kill> 0docker statsmontre un plateau proche de la limite
Correction : augmenter la limite, ou réduire la consommation (voir sections suivantes).
6.2 Confusion entre RSS et cache (page cache)
Le kernel utilise la RAM libre pour mettre en cache des fichiers (page cache). Dans un cgroup, ce cache compte dans l’usage mémoire.
Si votre conteneur fait beaucoup d’I/O (serveur web, base de données, traitement de fichiers), le file/cache peut monter.
Diagnostic :
- cgroup v2 :
memory.statmontrefileélevé,anonmodéré - cgroup v1 :
cacheélevé,rssmodéré
Actions possibles :
- Augmenter la limite si le cache est utile (performance).
- Réduire le cache côté application (buffering).
- Vérifier les patterns I/O (ex. lecture de gros fichiers).
- Pour certains workloads, mettre en place
memory.high(v2) pour throttler avant OOM.
6.3 mmap et fichiers mappés (ex : Lucene/Elasticsearch, bases, ML)
Les fichiers mappés en mémoire peuvent apparaître différemment selon outils. Une application peut “utiliser” beaucoup via mmap (fichiers index) et déclencher une pression mémoire.
Diagnostic :
memory.stat:fileetmapped_fileimportants (v1), oufileélevé (v2).- Dans le conteneur,
pmap -x <pid>(si disponible) montre de gros mappings.
Exemple :
docker exec -it myapp sh -lc 'ps aux | head'
docker exec -it myapp sh -lc 'pmap -x 1 | tail -n 20'
6.4 Fuite mémoire applicative (heap qui grossit sans redescendre)
Typique pour :
- Java (mauvais dimensionnement heap / fuite)
- Node.js (objets retenus)
- Python (caches, références cycliques, fuites natives)
- Go (retention, gros slices/maps)
Diagnostic :
anon/rssaugmente continuellement- pics de GC sans baisse significative
- OOM après un certain temps stable
Actions :
- profiler (heap dump, pprof, etc.)
- limiter la heap (par ex. JVM
-Xmx, Node--max-old-space-size) - corriger le code
6.5 Surconsommation “native” (hors heap) : threads, direct buffers, libc, etc.
Exemples :
- Java :
-XX:MaxDirectMemorySize, stacks de threads, metaspace - Python : extensions C, numpy, etc.
- Node : buffers natifs
Diagnostic :
- heap semble “ok” mais RSS explose
- beaucoup de threads
pmapmontre des segments anon importants
Dans le conteneur :
docker exec -it myapp sh -lc 'cat /proc/1/status | egrep "VmRSS|VmSize|Threads"'
7) Reproduire et observer : outils de mesure utiles
7.1 Mesurer la mémoire process par process
Dans le conteneur :
docker exec -it myapp sh -lc 'ps -eo pid,ppid,comm,rss,vsz,%mem --sort=-rss | head -n 20'
RSS en KiB. Cela aide à identifier le processus qui grossit.
7.2 Installer des outils (si image minimaliste)
Si vous êtes sur Debian/Ubuntu :
docker exec -it myapp sh -lc 'apt-get update && apt-get install -y procps psmisc'
Alpine :
docker exec -it myapp sh -lc 'apk add --no-cache procps'
7.3 Observer l’évolution dans le temps
Une boucle simple :
while true; do
date
docker stats --no-stream myapp
sleep 5
done
Ou au niveau cgroup v2 :
CG="/sys/fs/cgroup/system.slice/docker-$CID.scope"
watch -n 1 "cat $CG/memory.current; echo; cat $CG/memory.events"
8) Corriger : stratégies concrètes et commandes Docker
8.1 Définir des limites mémoire adaptées
Exemple simple :
docker run --name myapp --memory=2g --cpus=2 myimage:latest
Pour docker compose (sans YAML ici), l’idée est la même : mettre une limite mémoire explicite au niveau du runtime (selon votre orchestrateur).
Conseil : prévoyez une marge. Une app qui “tourne” à 1.2 Go peut avoir des pics à 1.8 Go lors de GC, chargement, ou pics traffic.
8.2 Comprendre --memory-swap
Selon la configuration, --memory-swap peut :
- être
memory + swap(limite totale) - ou être désactivé/ignoré si swap désactivé sur l’hôte
Exemples (à adapter à votre version Docker) :
- Limiter RAM à 1G et total RAM+swap à 2G :
docker run --memory=1g --memory-swap=2g myimage
- Interdire le swap (souvent utile pour latence prévisible) :
docker run --memory=1g --memory-swap=1g myimage
Attention : interdire le swap peut augmenter la probabilité d’un OOM si la limite est trop serrée.
8.3 Utiliser --memory-reservation (soft limit)
Permet d’indiquer une cible “souple” :
docker run --memory=2g --memory-reservation=1g myimage
Quand le système est sous pression, le kernel cherchera à récupérer de la mémoire de ce cgroup.
8.4 Ne pas désactiver l’OOM killer à l’aveugle
--oom-kill-disable peut empêcher le kernel de tuer un processus dans le conteneur, mais cela peut dégrader l’hôte (thrashing, blocages). À éviter sauf cas très maîtrisé.
9) Corriger côté application : réglages par langage (exemples pratiques)
9.1 Java (JVM) : dimensionner heap et mémoire native
Dans un conteneur, Java moderne détecte souvent les limites cgroup, mais ce n’est pas toujours suffisant.
Exemple de réglage explicite :
JAVA_OPTS="-Xms512m -Xmx1024m -XX:MaxMetaspaceSize=256m -XX:MaxDirectMemorySize=256m"
docker run --memory=2g -e JAVA_OPTS="$JAVA_OPTS" myjavaapp
Si vous utilisez un entrypoint qui lance java, assurez-vous qu’il applique JAVA_OPTS.
Pour diagnostiquer :
- activer logs GC
- faire un heap dump avant OOM (selon possibilité)
- vérifier le nombre de threads
Dans le conteneur :
docker exec -it myjavaapp sh -lc 'jcmd 1 VM.flags 2>/dev/null || true'
(Disponibilité selon JDK/JRE et outils inclus.)
9.2 Node.js : limiter le heap V8
Node peut dépasser la limite conteneur si non ajusté.
Exemple :
NODE_OPTIONS="--max-old-space-size=768"
docker run --memory=1g -e NODE_OPTIONS="$NODE_OPTIONS" mynodeapp
Diagnostiquer la fuite :
- snapshots heap
- inspection via
--inspect(en environnement contrôlé)
9.3 Python : attention aux bibliothèques natives et aux caches
Python “pur” est rarement la seule source : numpy/pandas/torch peuvent allouer en natif.
Actions :
- réduire la taille des batchs
- limiter le nombre de workers
- surveiller la mémoire RSS plutôt que seulement les objets Python
Dans le conteneur :
docker exec -it mypyapp sh -lc 'python -c "import os,psutil; p=psutil.Process(os.getpid()); print(p.memory_info())"'
(Nécessite psutil.)
9.4 Go : limiter la mémoire via GOMEMLIMIT (Go 1.19+)
Exemple :
docker run --memory=1g -e GOMEMLIMIT=800MiB mygoapp
Cela aide le GC à viser une limite.
10) Cas particulier : OOM global de l’hôte (pas seulement du conteneur)
Si docker inspect montre .OOMKilled=false mais le kernel a tué un process, vous êtes peut-être face à un OOM global.
10.1 Vérifier la mémoire hôte
free -h
vmstat 1 5
swapon --show
Sur un hôte sans swap, une montée de charge peut provoquer un OOM plus brutal.
10.2 Identifier le processus tué
Dans dmesg/journalctl -k, vous verrez le PID et le nom. Ensuite, corrélez avec le conteneur :
docker top myapp
Ou utilisez ps hôte + cgroup.
10.3 Corriger côté hôte
- Ajouter de la RAM
- Ajouter/activer du swap (avec prudence)
- Mettre des limites mémoire sur les conteneurs “bruyants”
- Éviter la surallocation (trop de conteneurs sans limites)
11) Mettre en place une prévention : alerting et observabilité
11.1 Surveiller les événements OOM
Vous pouvez suivre les événements Docker :
docker events --filter 'event=oom' --since 1h
Et les redémarrages :
docker events --filter 'event=die' --filter 'event=restart'
11.2 Exporter des métriques cgroup
En production, utilisez une stack de monitoring (Prometheus + node_exporter/cadvisor, ou l’équivalent). L’objectif : distinguer
anonvsfilememory.currentvsmemory.max- compteurs
oom_kill
Même sans stack complète, vous pouvez déjà automatiser une collecte :
CID=$(docker inspect -f '{{.Id}}' myapp)
CG="/sys/fs/cgroup/system.slice/docker-$CID.scope"
sudo bash -lc "date; echo memory.current=$(cat $CG/memory.current); echo memory.max=$(cat $CG/memory.max); cat $CG/memory.events"
12) Checklist de diagnostic rapide (pratique)
-
Le conteneur a-t-il été OOMKilled ?
docker inspect myapp --format '{{.State.OOMKilled}}' -
Le kernel confirme-t-il un OOM (memcg ou global) ?
sudo journalctl -k --since "2 hours ago" | egrep -i 'oom|killed process|out of memory' -
Quelle est la limite mémoire effective ?
docker inspect myapp --format 'Memory={{.HostConfig.Memory}} MemorySwap={{.HostConfig.MemorySwap}}' -
Quelle composante gonfle : anon (heap) ou file (cache) ?
- cgroup v2 :
# adapter le chemin CG cat $CG/memory.stat | egrep 'anon |file |slab |kernel_stack'
- cgroup v2 :
-
Quel process consomme le plus ?
docker exec -it myapp sh -lc 'ps -eo pid,comm,rss --sort=-rss | head' -
Correction : augmenter limite / ajuster heap / corriger fuite / réduire cache / ajouter swap / limiter concurrence.
13) Exemple guidé : conteneur qui crash en code 137
13.1 Constat
docker ps -a
# myapp Exited (137) ...
docker inspect myapp --format 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}'
# OOMKilled=true ExitCode=137
13.2 Kernel
sudo dmesg -T | tail -n 200 | egrep -i 'oom|killed process'
# Memory cgroup out of memory: Kill process 1234 (node) score 987 ...
=> OOM dans le cgroup.
13.3 Limites
docker inspect myapp --format 'Memory={{.HostConfig.Memory}} MemorySwap={{.HostConfig.MemorySwap}}'
# Memory=536870912 (512MiB)
13.4 Correction
- Ajuster Node :
docker rm -f myapp
docker run -d --name myapp --memory=1g -e NODE_OPTIONS="--max-old-space-size=768" mynodeapp:latest
- Vérifier :
docker stats myapp
14) Points d’attention fréquents (pièges)
- Images minimalistes : pas d’outils (
ps,pmap,top). Prévoyez une image debug ou sidecar. - Interprétation de
docker stats: la “MEM USAGE” inclut souvent cache et peut surprendre. - Swap désactivé : OOM plus probable. À évaluer selon SLO (latence vs stabilité).
- Overcommit : Linux peut autoriser des allocations virtuelles qui échouent plus tard (OOM).
- Orchestrateurs : Kubernetes ajoute ses propres notions (requests/limits). Les OOMKilled y sont très courants si
limitstrop bas.
Conclusion
Diagnostiquer un Docker OOM efficacement revient à répondre à trois questions :
- Qui a déclenché l’OOM ? (cgroup du conteneur vs OOM global de l’hôte)
- Quelle mémoire a gonflé ? (
anon/RSS vsfile/cache vsmmapvs natif) - Quelle correction est la plus saine ? (limite mieux calibrée, réglage heap, réduction concurrence, correction fuite, swap, observabilité)
Avec les commandes docker inspect, journalctl -k/dmesg et la lecture des métriques cgroup, vous pouvez passer d’un crash “mystérieux” à une cause précise et une action corrective durable.
Si vous me donnez :
- la sortie de
docker inspect ...Stateet...HostConfig.Memory*, - un extrait de
journalctl -kautour de l’OOM, - et
memory.stat/memory.events(v2) oumemory.stat/memory.failcnt(v1),
je peux vous aider à interpréter exactement quel type de mémoire déclenche l’OOM et proposer un plan de correction ciblé.