Limites mémoire Docker Compose pour éviter le code 137
Définissez les limites mémoire et CPU dans Docker Compose pour isoler un conteneur gourmand, éviter l’OOM sur votre VPS et comprendre deploy.resources, mem_limit et swap.
Ce que fait une limite de mémoire Docker Compose
Une limite de mémoire Docker Compose est une valeur maximale stricte que le noyau Linux applique au cgroup (groupe de contrôle, fonctionnalité du noyau qui mesure les ressources d’un ensemble de processus) d’un conteneur. Définissez deploy.resources.limits.memory pour un service : ce conteneur ne pourra jamais utiliser plus que la valeur indiquée. Lorsqu’il tente de la dépasser, le noyau tue un processus dans le conteneur. Le conteneur se termine 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ù l’hôte ne dispose d’aucune mémoire supplémentaire à prêter. Un conteneur qui subit une fuite mémoire ou exécute une requête défectueuse peut utiliser toutes les pages libres d’un serveur avec 8GB de RAM. Le noyau tue alors le processus qu’il juge 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 l’arrêt d’un seul service, qui peut redémarrer.
services:
app:
image: ghcr.io/example/app:1.4
deploy:
resources:
limits:
cpus: "1.5"
memory: 1g
reservations:
memory: 256mAppliquez la configuration, puis vérifiez que la limite est active :
docker compose up -d
docker stats --no-streamLa colonne MEM USAGE / LIMIT doit afficher une valeur similaire à 142MiB / 1GiB. Si la colonne de limite affiche la totalité de la RAM de l’hôte, le paramètre n’a pas été appliqué. Le reste de ce guide ne pourra pas vous aider 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 syntaxes existent pour la même notion, 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 désormais partie de la Compose Specification, le format que docker compose lit aujourd’hui.
Les deux fonctionnent sur un seul hôte. Docker Compose, le plugin docker compose, applique deploy.resources.limits et deploy.resources.reservations lorsque vous exécutez docker compose up, sans aucun cluster Swarm. Le plugin est désormais en version 5 (v5.6.0 était la dernière version stable lors de la vérification, le 2026-10-06). Il est passé directement de la version 2 à la version 5, en sautant les versions 3 et 4 pour éviter toute confusion avec les anciennes versions du format de fichier Compose. Aucune version 5 n’a modifié la gestion des limites de ressources. 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 syntaxe par projet. Écrire mem_limit: 512m et deploy.resources.limits.memory: 1g pour le même service produit un fichier dont le comportement est difficile à déterminer d’un coup d’œil. Au lieu de deviner quelle valeur est retenue, interrogez le daemon :
docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}} {{.HostConfig.NanoCpus}}' app-1Les valeurs de mémoire sont exprimées en octets. Ainsi, 1g s’affiche sous la forme 1073741824. Le CPU est exprimé en nano-CPU. Ainsi, 1.5 s’affiche sous la forme 1500000000. La valeur 0 dans un champ signifie qu’aucune limite n’a été définie. La plus petite limite mémoire 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 noyau 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, 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 n’importe quel 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-1true 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=oomLisez ensuite le journal du noyau, qui conserve les traces 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 correspond à un OOM de l’hôte. La machine elle-même n’avait alors plus de RAM disponible. C’est précisément l’incident 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 défaillante, afin qu’un conteneur qui s’arrête continuellement soit visible sans surveillance manuelle.
La réservation est un indicateur ; la limite fait foi
reservations.memory (l’ancien mem_reservation) constitue un seuil souple. Docker la 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 en aura besoin. 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.
Comptabiliser la swap, sans se mentir
La plupart des images VPS sont livrées sans aucun fichier de swap. Exécutez swapon --show et free -h. Si le total de swap est nul, aucun des paramètres liés à la swap ci-dessous n’a d’effet, et votre limite mémoire correspond uniquement à la RAM.
memswap_limit ne désigne pas la quantité de swap. Il s’agit du total de la mémoire et de la 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 n’a aucune 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. La 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 provient des 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 concernant la swap est ignorée.
Soyez précis sur ce que la swap vous apporte. Elle ralentit un OOM kill, mais ne le rend pas moins probable, car un processus qui fuit de la mémoire remplit la swap aussi facilement que la RAM. Par ailleurs, un conteneur qui sature la 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 de la mémoire semble pire qu’elle ne l’est
La valeur MEM USAGE dans docker stats inclut le page cache. Un conteneur qui lit de gros fichiers se rapproche donc de sa limite, puis y reste. 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é semblera rester en permanence proche de sa limite pour cette raison précise.
Séparez le cache du working set 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.eventsanon correspond à la mémoire anonyme, c’est-à-dire au working set qui ne peut pas être libéré. file correspond au page cache, 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 surengagement 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 léger, et une limite aussi basse permet de détecter immédiatement une relecture de configuration incontrôlée.
- PostgreSQL : limite de 2g, avec
shared_buffersdé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 médias ou de fichiers : limite de 2g, dont la majeure partie servira de page cache.
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 comme 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 remarque distincte. La plupart des runtimes ne voient pas la limite tant que 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 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 heap grossir jusqu’à l’intervention du kernel. Ollama suit le même principe avec un autre paramètre, car augmenter num_ctx augmente le cache KV de plusieurs centaines de mégaoctets et le conteneur meurt au milieu d’un long prompt. Le cgroup ne négocie pas. Il tue.
Les limites CPU fonctionnent 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 par période de 100 ms, répartis entre tous ses threads. Lorsqu’il a épuisé ce temps, le kernel le met en attente 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 n’a d’effet que lorsque les CPU sont réellement saturés. Deux conteneurs avec des shares de 1024 et 512 se répartissent un cœur très sollicité 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. Docker Compose, le plugin docker compose (désormais en version majeure 5), 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 noyau est la cause la plus fréquente, mais un délai d’arrêt expiré produit le même code lorsqu’une application ignore SIGTERM. Exécutez docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' <container> pour les distinguer. 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 sur 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 de docker stats inclut le cache de pages. Le noyau le 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 lisez la valeur anon, qui correspond au working set non récupérable. Une valeur file élevée à côté d’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 noyau, le daemon Docker, sshd, journald et votre propre shell. Gardez ensuite la somme des limites de tous les conteneurs sous les 7GB restants. Surveillez la valeur maximale de anon par conteneur pendant une journée, avec une charge réelle, avant de fixer vos valeurs. Considérez le total comme un budget, et non comme une capacité à remplir.