← Retour aux tutoriels

Docker Out of Memory (OOM) : diagnostiquer et corriger les crashs mémoire des conteneurs

dockeroomoom-killermemoireconteneursdevopslinuxmonitoringkubernetesperformance

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 :

  1. 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.

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

Ensuite :

docker inspect <container_id_or_name> --format '{{json .State}}' | jq

Points clés :

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 :

Interprétation rapide :


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 :

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

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 :

Exemple d’interprétation :

Dans memory.stat, regardez notamment :


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 :


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 :

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 :

Actions possibles :


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 :

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 :

Diagnostic :

Actions :


6.5 Surconsommation “native” (hors heap) : threads, direct buffers, libc, etc.

Exemples :

Diagnostic :

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 :

Exemples (à adapter à votre version Docker) :

docker run --memory=1g --memory-swap=2g myimage
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 :

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 :

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 :

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


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

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)

  1. Le conteneur a-t-il été OOMKilled ?

    docker inspect myapp --format '{{.State.OOMKilled}}'
  2. 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'
  3. Quelle est la limite mémoire effective ?

    docker inspect myapp --format 'Memory={{.HostConfig.Memory}} MemorySwap={{.HostConfig.MemorySwap}}'
  4. Quelle composante gonfle : anon (heap) ou file (cache) ?

    • cgroup v2 :
      # adapter le chemin CG
      cat $CG/memory.stat | egrep 'anon |file |slab |kernel_stack'
  5. Quel process consomme le plus ?

    docker exec -it myapp sh -lc 'ps -eo pid,comm,rss --sort=-rss | head'
  6. 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

docker rm -f myapp
docker run -d --name myapp --memory=1g -e NODE_OPTIONS="--max-old-space-size=768" mynodeapp:latest
docker stats myapp

14) Points d’attention fréquents (pièges)


Conclusion

Diagnostiquer un Docker OOM efficacement revient à répondre à trois questions :

  1. Qui a déclenché l’OOM ? (cgroup du conteneur vs OOM global de l’hôte)
  2. Quelle mémoire a gonflé ? (anon/RSS vs file/cache vs mmap vs natif)
  3. 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 :

je peux vous aider à interpréter exactement quel type de mémoire déclenche l’OOM et proposer un plan de correction ciblé.