SSD Nodes Learn 8GB de RAM — $66/an
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-02

Limites mémoire Docker Compose pour éviter l’OOM

Définissez des limites mémoire et CPU avec deploy.resources ou mem_limit. Comprenez l’erreur exit 137, le swap et le dimensionnement sur un VPS de 8 GB.

Ce que fait une limite de mémoire Docker Compose

Une limite de mémoire Docker Compose est une limite stricte imposée par le noyau Linux au cgroup (groupe de contrôle, une fonctionnalité du noyau qui mesure les ressources d’un ensemble de processus) d’un conteneur. Définissez deploy.resources.limits.memory sur un service : ce conteneur ne pourra jamais utiliser plus que la valeur indiquée. Lorsqu’il essaie de la dépasser, le noyau tue un processus dans le conteneur, qui se termine généralement avec le code 137.

Cela compte surtout sur un VPS, où la quantité de RAM est fixe et où aucune mémoire supplémentaire de l’hôte n’est disponible. Un conteneur qui présente une fuite mémoire ou exécute une requête incorrecte peut utiliser toutes les pages libres d’un serveur de 8GB. Le noyau tue alors le processus qu’il considère comme le plus problématique. Il s’agit souvent d’une base de données ou de votre session SSH, plutôt que du conteneur à l’origine du problème. Les limites transforment une panne de tout le serveur en un problème limité à un service qui redémarre.

services:
  app:
    image: ghcr.io/example/app:1.4
    deploy:
      resources:
        limits:
          cpus: "1.5"
          memory: 1g
        reservations:
          memory: 256m

Appliquez la limite et vérifiez qu’elle est active :

docker compose up -d
docker stats --no-stream

La colonne MEM USAGE / LIMIT devrait afficher une valeur du type 142MiB / 1GiB. Si la colonne de limite affiche la totalité de la RAM de l’hôte, le paramètre ne s’est pas appliqué. Le reste de ce guide ne vous aidera pas tant que ce problème ne sera pas résolu. Si le fichier Compose vous est encore inconnu, les bases de Docker Compose pour un VPS présentent la structure de fichier utilisée ici.

deploy.resources.limits ou mem_limit : lequel s’applique

Deux écritures existent pour la même notion, ce qui explique cette confusion.

mem_limit, mem_reservation, memswap_limit, cpus et cpu_shares sont des clés de service de premier niveau héritées des anciens formats de fichiers Compose. deploy.resources vient du schéma Swarm et fait maintenant partie de la Compose Specification, le format que docker compose lit aujourd’hui.

Les deux fonctionnent sur un hôte unique. Compose V2, le plugin docker compose, applique deploy.resources.limits et deploy.resources.reservations lorsque vous exécutez docker compose up, sans aucun cluster Swarm. Les éléments réservés à Swarm dans le bloc deploy sont les autres clés : mode, placement, update_config et endpoint_mode ont une signification pour docker stack deploy et sont ignorées par docker compose up. Le conseil courant selon lequel « deploy nécessite Swarm » est donc incorrect pour la sous-section resources. Le suivre laisse vos services sans aucune limite.

Choisissez une seule écriture par projet. Écrire mem_limit: 512m et deploy.resources.limits.memory: 1g sur le même service produit un fichier dont le comportement est difficile à comprendre au premier coup d’œil. Au lieu de deviner quelle valeur est appliquée, interrogez le daemon :

docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}} {{.HostConfig.NanoCpus}}' app-1

Les valeurs de mémoire sont exprimées en octets. Ainsi, 1g affiche 1073741824. Le CPU est exprimé en nano-CPU. Ainsi, 1.5 affiche 1500000000. Une valeur 0 dans un champ signifie qu’aucune limite n’a été définie. La limite mémoire minimale acceptée par Docker est 6m. En dessous de cette valeur, le conteneur refuse de démarrer.

Ce qui se passe lorsqu’un conteneur atteint la limite

Le conteneur ne ralentit pas. Il s’arrête.

Lorsqu’un processus demande une page et que le cgroup a déjà atteint sa memory.max, le kernel récupère d’abord ce qu’il peut à l’intérieur de ce cgroup : le page cache propre, puis les pages qu’il peut envoyer dans le swap. Si cette récupération ne libère pas assez de mémoire, le tueur OOM (out of memory) du cgroup sélectionne un processus à l’intérieur du conteneur et lui envoie SIGKILL. Tuer le PID 1 du conteneur arrête le conteneur. Le code de sortie 137 correspond simplement à 128 plus le signal 9. Ainsi, 137 indique la présence d’un SIGKILL, mais ne prouve pas à lui seul un OOM.

docker compose ps -a
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app-1

true 137 correspond à un OOM kill. false 137 signifie qu’un autre élément a envoyé SIGKILL. La cause habituelle est que docker compose stop a atteint son délai de grâce de dix secondes parce que l’application a ignoré SIGTERM. Cette distinction fait gagner des heures, car ces deux problèmes n’ont rien en commun.

Deux autres emplacements enregistrent l’événement. Surveillez le daemon en temps réel :

docker events --filter event=oom

Lisez ensuite le kernel log, qui conserve l’événement après un redémarrage :

sudo dmesg -T | grep -i -E 'memory cgroup out of memory|killed process'

Un kill de cgroup affiche une ligne commençant par Memory cgroup out of memory: Killed process 24713 (node). Une ligne sans le préfixe Memory cgroup indique un OOM de l’hôte. Cela signifie que la machine elle-même n’a plus assez de RAM. C’est précisément le problème que les limites doivent empêcher. Sa présence indique donc que la somme de vos limites est trop élevée ou que certains services n’ont aucune limite.

Avec restart: unless-stopped, une boucle d’OOM peut facilement passer inaperçue, car le service apparaît comme actif dans docker compose ps une seconde après son arrêt. Vérifiez la colonne d’uptime et le compteur de redémarrages. Associez aussi la limite à un healthcheck qui indique que l’application est unhealthy afin qu’un conteneur qui s’arrête continuellement soit visible sans surveillance manuelle.

Une réservation est un indicateur, la limite est la règle

reservations.memory (l’ancien mem_reservation) constitue un seuil souple. Docker le décrit comme une limite souple activée lorsque le daemon détecte une contention ou une faible quantité de mémoire disponible sur l’hôte. Elle n’empêche jamais un conteneur de la dépasser et ne garantit jamais que la mémoire sera disponible lorsque le conteneur la demandera. Elle indique seulement au kernel de récupérer en priorité la mémoire des conteneurs qui dépassent leur réservation.

Une réservation ne protège donc rien à elle seule. Utilisez-la pour signaler un service à traiter en priorité sous pression, et appuyez-vous sur la limite pour assurer la sécurité. La réservation doit rester inférieure à la limite, sinon le conteneur ne démarrera pas : Docker rejette la configuration avec Minimum memory limit can not be less than memory reservation limit.

La comptabilisation du swap, en toute transparence

La plupart des images VPS sont livrées sans fichier swap. Exécutez swapon --show et free -h. Si le total du swap est égal à zéro, tous les paramètres liés au swap ci-dessous sont sans effet, et votre limite mémoire est strictement une limite de RAM.

memswap_limit ne correspond pas à la quantité de swap. Il s’agit du total de la mémoire et du swap. Avec mem_limit: 1g et memswap_limit: 2g, le conteneur dispose de 1GB de RAM et de 1GB de swap. Si les deux valeurs sont identiques, le conteneur ne dispose d’aucun swap. Définir mem_limit et laisser memswap_limit non défini permet de nouveau au conteneur d’utiliser jusqu’à la taille de sa limite mémoire en swap.

Ubuntu 24.04 et Debian 13 utilisent cgroup v2 par défaut. Le swap y est un compteur distinct (memory.swap.max), et cela fonctionne sans configuration supplémentaire. L’ancien message Your kernel does not support swap limit capabilities apparaît sur les hôtes utilisant cgroup v1 et démarrés sans swapaccount=1. Sur ces hôtes, la limite mémoire s’applique toujours, mais la partie swap est ignorée.

Soyez précis sur ce que le swap apporte. Il ralentit un OOM kill, mais ne le rend pas moins probable, car un processus qui fuit remplit le swap aussi facilement que la RAM. De plus, un conteneur qui utilise intensivement le swap sur le stockage VPS partagé ralentit tous les autres services de la machine. Pour tout service sensible à la latence, une limite correcte sans swap provoque un échec plus rapide et plus prévisible.

Pourquoi l’utilisation de la mémoire semble pire qu’elle ne l’est

La valeur MEM USAGE dans docker stats inclut le cache de pages. Un conteneur qui lit de gros fichiers augmente donc sa consommation jusqu’à sa limite et y reste. C’est normal. Il ne s’agit pas d’une fuite, car le cache propre est récupéré avant même que l’OOM killer soit appelé. Un service comme un serveur multimédia Jellyfin auto-hébergé semblera rester en permanence proche de sa limite pour cette raison précise.

Séparez le cache de l’ensemble de travail réel depuis l’intérieur du conteneur :

docker compose exec app grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat
docker compose exec app cat /sys/fs/cgroup/memory.events

anon correspond à la mémoire anonyme, c’est-à-dire à l’ensemble de travail qui ne peut pas être libéré. file correspond au cache de pages, qui peut l’être. Dimensionnez votre limite en fonction de anon, avec une marge, et non en fonction du total. Le fichier memory.events permet de trancher : un compteur oom_kill supérieur à zéro signifie que le kernel a tué un processus dans ce conteneur depuis son démarrage, tandis qu’un compteur max qui augmente signifie que le conteneur atteint actuellement sa limite. Les deux commandes nécessitent un shell et coreutils dans l’image. Elles échouent donc avec une image distroless ou scratch.

Limites de dimensionnement sur un VPS de 8GB

Commencez par l’hôte, pas par les applications. Sur un VPS de 8GB, réservez environ 1GB pour le kernel, le daemon Docker, sshd, journald et votre propre shell de connexion. Il reste donc environ 7GB à répartir, et la somme des limites de tous les conteneurs doit rester inférieure à cette valeur. Le surbooking fonctionne jusqu’au jour où deux services atteignent leur pic en même temps.

Répartition possible sur une machine de 8GB :

  • Reverse proxy : limite de 128m. C’est un processus peu gourmand, et une limite aussi stricte détecte immédiatement un rechargement de configuration incontrôlé.
  • PostgreSQL : limite de 2g, avec shared_buffers défini à environ 512MB dans la configuration de la base de données.
  • Conteneur d’application : limite de 1g.
  • Worker d’arrière-plan : limite de 512m.
  • Service multimédia ou de fichiers : limite de 2g, dont la majeure partie sera utilisée par le cache de pages.

Ne recopiez pas ces valeurs dans votre propre stack. Faites fonctionner les services avec une charge réelle pendant une journée, surveillez docker stats, relevez la valeur maximale de anon pour chaque conteneur, puis ajoutez environ la moitié de cette valeur comme marge. Une limite trop stricte est pire que l’absence de limite, car elle arrête un service sain pendant un pic normal de trafic réseau.

Un piège mérite une remarque distincte. La plupart des runtimes ignorent la limite si vous ne la leur indiquez pas. PostgreSQL dimensionnera volontiers shared_buffers et work_mem au-delà de la limite de son conteneur, puis sera tué. Une JVM (machine virtuelle Java) a besoin de -XX:MaxRAMPercentage=75 pour dimensionner son heap à partir de la limite du cgroup, et non de la RAM de l’hôte. Node.js a besoin de --max-old-space-size en mégaoctets, avec une valeur inférieure à la limite du conteneur, sinon son garbage collector laisse le heap croître jusqu’à l’intervention du kernel. Le cgroup ne négocie pas. Il tue.

Les limites CPU se comportent de manière complètement différente

cpus: "1.5" correspond à 150 % d’un cœur, appliqués sous forme de quota CFS (completely fair scheduler). Le conteneur dispose de 150 ms de temps CPU sur chaque période de 100 ms, réparties entre tous ses threads. Une fois cette durée consommée, le kernel le fait attendre jusqu’à la période suivante.

C’est la différence importante. Un conteneur qui dépasse sa limite mémoire est tué. Un conteneur qui dépasse sa limite CPU est bridé et continue de fonctionner, plus lentement. Vous pouvez donc définir une limite CPU de manière stricte, tandis qu’une limite mémoire doit conserver une marge.

cpu_shares est un autre mécanisme : un poids relatif qui ne compte que lorsque les CPU sont réellement saturés. Deux conteneurs dont les shares sont de 1024 et 512 se répartissent un cœur occupé selon un rapport approximatif de deux pour un. Sur une machine inactive, aucun des deux n’est limité. Utilisez les shares pour définir la priorité des services, et cpus lorsque vous avez besoin d’un plafond réel, par exemple pour empêcher une tâche de transcodage nocturne de priver votre serveur web de ressources.

FAQ

deploy.resources.limits fonctionne-t-il sans Docker Swarm ?

Oui. Compose V2 applique deploy.resources.limits et deploy.resources.reservations lorsque vous exécutez docker compose up sur un hôte unique. Vérifiez-le avec docker inspect --format '{{.HostConfig.Memory}}' <container>, qui affiche la limite en octets et affiche 0 lorsqu’aucune limite n’a été appliquée. Les clés de deploy qui nécessitent réellement Swarm sont mode, placement, update_config et endpoint_mode.

Que signifie le code de sortie 137 dans Docker Compose ?

Le processus principal a reçu SIGKILL, car 137 correspond à 128 plus le signal 9. Le tueur OOM du kernel est la cause la plus courante, mais un délai d’arrêt dépassé produit le même code lorsqu’une application ignore SIGTERM. Exécutez docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' <container> pour les différencier. true 137 indique un kill dû à la mémoire, contrairement à false 137.

Dois-je définir mem_limit ou deploy.resources.limits.memory ?

Les deux fonctionnent avec docker compose. deploy.resources.limits.memory est la forme actuelle de la Compose Specification et constitue le meilleur choix par défaut pour un nouveau fichier. Conservez mem_limit si le reste de votre fichier utilise déjà les anciennes clés de niveau supérieur. Définir les deux pour un même service rend seulement le fichier plus difficile à lire. Choisissez donc l’une des deux options et vérifiez le résultat avec docker inspect.

Pourquoi mon conteneur atteint-il sa limite mémoire maximale sans être tué ?

La valeur d’utilisation dans docker stats inclut le cache de pages, que le kernel libère sous pression au lieu de déclencher un kill OOM. Exécutez docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat et consultez la valeur anon, qui correspond à l’ensemble de travail non récupérable. Une valeur file élevée associée à une valeur anon faible indique que le conteneur effectue des entrées-sorties disque, et non qu’il est sur le point de s’arrêter.

Quelle quantité de RAM dois-je laisser non allouée sur un VPS de 8GB ?

Laissez environ 1GB pour le kernel, le daemon Docker, sshd, journald et votre propre shell. Maintenez ensuite la somme des limites de tous les conteneurs sous les 7GB restants. Surveillez la valeur de pointe anon de chaque conteneur pendant une journée, sous une charge réelle, avant de fixer les valeurs. Considérez le total comme un budget, et non comme une quantité à remplir.