SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-22

Docker Compose : limiter la mémoire pour éviter l’OOM

Définissez des limites mémoire et CPU avec deploy.resources ou mem_limit pour éviter l’erreur exit 137 et empêcher un conteneur de saturer votre VPS.

Ce que fait une limite mémoire Docker Compose

Une limite mémoire Docker Compose est un plafond strict que le noyau Linux applique au cgroup (groupe de contrôle, une fonction 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 s’arrête généralement avec le code 137.

Cette limite est particulièrement importante sur un VPS, où la quantité de RAM est fixe et où aucune mémoire supplémentaire n’est disponible sur l’hôte. Un conteneur qui présente une fuite mémoire ou exécute une requête défectueuse peut consommer toutes les pages libres d’un serveur de 8GB. Le noyau tue alors le processus qu’il considère comme le moins prioritaire. 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 l’ensemble du serveur en un problème limité à un service, qui peut redémarrer.

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

Appliquez la configuration et vérifiez que la limite est active :

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

La colonne MEM USAGE / LIMIT doit afficher une valeur similaire à 142MiB / 1GiB. Si la colonne de limite indique la totalité de la RAM de l’hôte, la configuration ne s’est pas appliquée. La suite de ce guide ne vous aidera pas tant que ce problème ne sera pas corrigé. Si le fichier Compose vous est encore inconnu, les bases de Docker Compose pour un VPS expliquent la structure de fichier utilisée ici.

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

Deux syntaxes existent pour la même idée, ce qui prête à 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 provient du schéma Swarm et fait maintenant partie de la Compose Specification, le format que docker compose lit actuellement.

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 du 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 faux pour la sous-section resources. Le suivre laisse vos services sans aucune limite.

Choisissez une seule syntaxe par projet. Écrire mem_limit: 512m et deploy.resources.limits.memory: 1g sur le même service produit un fichier dont le contenu est difficile à comprendre au premier coup d’œil. Au lieu de deviner quelle valeur a été retenue, 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. La valeur 0 dans un champ signifie qu’aucune limite n’a été définie. La limite de mémoire minimale acceptée par Docker est 6m. En dessous de cette valeur, le conteneur refuse de démarrer.

Que se passe-t-il 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 suffisamment de mémoire, l’OOM killer 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. La valeur 137 indique donc un SIGKILL, mais ne prouve pas à elle seule qu’il s’agit d’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.

L’événement est enregistré à deux autres endroits. Surveillez le daemon en temps réel :

docker events --filter event=oom

Lisez ensuite le journal du kernel, qui conserve les informations après un redémarrage :

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

Un kill provenant d’un cgroup affiche une ligne commençant par Memory cgroup out of memory: Killed process 24713 (node). Une ligne sans le préfixe Memory cgroup correspond à un OOM de l’hôte. La machine elle-même n’avait alors plus de RAM disponible. C’est précisément le problème que les limites doivent empêcher. Sa présence indique 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 nombre de redémarrages. Associez aussi la limite à un healthcheck qui indique que l’application est défaillante afin qu’un conteneur qui s’arrête en boucle reste visible sans devoir le surveiller.

Une réservation est un indice ; la limite fait foi

reservations.memory (l’ancien mem_reservation) est un seuil souple. Docker le décrit comme une limite souple qui s’active 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é lorsque la mémoire est 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 nul, tous les paramètres liés au swap ci-dessous sont sans effet, et votre limite mémoire est uniquement 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 une quantité de swap allant jusqu’à la taille de sa limite mémoire.

Ubuntu 24.04 et Debian 13 utilisent cgroup v2 par défaut. Le swap y est un compteur séparé (memory.swap.max), et cette configuration fonctionne sans réglage supplémentaire. L’ancien message Your kernel does not support swap limit capabilities provient des hôtes utilisant cgroup v1 et démarrés sans swapaccount=1. Sur ces hôtes, la limite mémoire continue de s’appliquer, 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. En parallèle, un conteneur qui utilise intensivement le swap sur le stockage VPS partagé ralentit tous les autres services du serveur. Pour tout service sensible à la latence, une limite correcte sans swap échoue plus rapidement et de manière plus prévisible.

Pourquoi l’utilisation mémoire paraît 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 atteint donc progressivement sa limite et s’y maintient. C’est normal et ce n’est pas une fuite mémoire, 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é paraît donc 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 est actuellement maintenu à 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 au noyau, au 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 surengagement fonctionne jusqu’au jour où deux services atteignent leur pic en même temps.

Une répartition raisonnable sur une machine de 8GB :

  • Reverse proxy : limite de 128m. C’est un processus léger, 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 applicatif : limite de 1g.
  • Worker d’arrière-plan : limite de 512m.
  • Service de fichiers ou de médias : 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 sous 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 pour la marge. Une limite trop basse est pire que l’absence de limite, car elle arrête un service sain pendant un pic normal de trafic.

Un piège mérite une note distincte. La plupart des runtimes ignorent la limite tant que vous ne la leur indiquez pas. PostgreSQL dimensionnera sans problème 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 tas à partir de la limite du cgroup plutôt qu’à partir 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 tas augmenter jusqu’à l’intervention du noyau. Ollama fonctionne de la même manière avec un autre paramètre, car augmenter num_ctx augmente le cache KV de centaines de mégaoctets et le conteneur s’arrête au milieu d’un prompt long. Le cgroup ne négocie pas. Il tue le processus.

Les limites CPU fonctionnent de manière très différente

cpus: "1.5" signifie 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, partagés entre tous ses threads. Une fois ce temps consommé, 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. Une limite CPU peut donc être définie de manière stricte sans risque particulier, tandis qu’une limite mémoire doit laisser une marge.

cpu_shares est un autre mécanisme : un poids relatif qui n’a d’importance que lorsque les CPU sont réellement saturés. Deux conteneurs avec des shares de 1024 et 512 se partagent un cœur chargé selon un rapport approximatif de deux pour un. Sur un serveur inactif, aucun des deux n’est limité. Utilisez les shares pour hiérarchiser les services selon leur importance, 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 seul hôte. 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 kernel OOM killer est la cause la plus fréquente, 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 correspond à un kill 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-en une 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 affichée par docker stats inclut le page cache, 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 au working set 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 pendant une journée, en charge réelle, la valeur maximale de anon pour chaque conteneur avant de fixer les limites. Considérez le total comme un budget, et non comme une capacité à remplir.