Limiter la mémoire Docker Compose pour éviter les OOM
Apprenez à configurer deploy.resources.limits.memory pour empêcher un conteneur de saturer votre VPS. Évitez l'erreur exit 137 et la chute du serveur en isolant la RAM.
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 fichier Compose. deploy.resources provient 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. 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 maintenant dans sa branche v5 (v5.6.0 était la dernière version stable lors de la vérification, le 2026-10-10). Il est passé directement de v2 à v5, en sautant les versions 3 et 4 pour éviter la confusion avec les anciennes versions de format de fichier Compose. Aucune version v5 n’a modifié la gestion des limites de ressources. Les éléments orientés Swarm du bloc deploy sont les autres clés, telles que placement, update_config et endpoint_mode, qui décrivent la planification et les déploiements progressifs sur un cluster de nœuds pour docker stack deploy. L’affirmation courante selon laquelle « deploy nécessite Swarm » est donc fausse pour la sous-section resources. La 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 ne nuit pas seulement à la lisibilité : la Compose Specification indique que lorsque mem_limit est défini, il doit être cohérent avec limits.memory, mais elle ne précise pas quelle valeur prévaut en cas de divergence. Quelle que soit la syntaxe utilisée, vérifiez ce que le daemon a réellement appliqué :
docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}} {{.HostConfig.NanoCpus}}' app-1Les valeurs de mémoire sont exprimées en octets. 1g s’affiche donc comme 1073741824. Le CPU est exprimé en nano-CPU. 1.5 s’affiche donc comme 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.
Ce qui se passe lorsqu’un conteneur atteint sa 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 commence par récupérer 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 réception de SIGKILL, mais ne prouve pas à lui seul qu’un OOM s’est produit.
docker compose ps -a
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app-1true 137 correspond à un OOM kill. false 137 signifie généralement qu’un autre élément a envoyé SIGKILL. Un programme peut aussi quitter volontairement avec le code 137, mais c’est rare. 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 kernel, qui conserve la trace après un redémarrage :
sudo dmesg -T | grep -i -E 'memory cgroup out of memory|killed process'Un kill de cgroup écrit une ligne qui commence par Memory cgroup out of memory: Killed process 24713 (node). Cette ligne signifie qu’un conteneur a atteint sa propre limite. Elle peut apparaître alors que l’hôte dispose encore de beaucoup de RAM libre. Une ligne sans le préfixe Memory cgroup indique un OOM de l’hôte. La machine elle-même n’avait alors plus de RAM disponible. C’est précisément la panne que les limites doivent empêcher. Si vous la voyez, cela signifie généralement que la somme de vos limites est trop élevée, que certains services n’ont aucune limite, ou qu’un élément extérieur à Docker utilise davantage de mémoire que la marge disponible.
Avec restart: unless-stopped, une boucle d’OOM peut 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 en boucle reste visible sans surveillance permanente.
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
Sous Linux, la valeur MEM USAGE dans docker stats ne soustrait que le cache de pages inactif (inactive_file avec cgroup v2). Le cache récemment utilisé est donc toujours comptabilisé. Un conteneur qui lit de gros fichiers augmente sa consommation jusqu’à approcher sa limite, puis y reste. C’est normal et ce n’est pas 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.
Distinguez le cache de la working set réelle 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 la working set qui ne peut pas être libérée. file correspond au cache de pages, qui peut l’être. Dimensionnez la limite en fonction de anon, avec une marge, et non en fonction du total. Le fichier memory.events permet de trancher définitivement : une valeur oom_kill supérieure à zéro signifie que le kernel a tué un processus dans ce conteneur depuis son démarrage, tandis qu’une valeur 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 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. La surallocation 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 stricte détecte immédiatement un rechargement de configuration incontrôlé.
- PostgreSQL : limite de 2g, avec
shared_buffersréglé sur environ 512MB dans la configuration de la base de données. - Conteneur applicatif : limite de 1g.
- Worker en arrière-plan : limite de 512m.
- Service de médias ou de fichiers : limite de 2g, dont la plus grande 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 pour la marge. Une limite trop basse est pire que l’absence de limite, car elle arrête un service sain lors d’un pic normal de trafic.
Un piège mérite une remarque distincte. Certains runtimes ignorent la limite tant que vous ne la leur indiquez pas. Ceux qui la lisent peuvent aussi ne pas dimensionner leurs ressources comme prévu. PostgreSQL peut dimensionner shared_buffers et work_mem au-delà de la limite de son conteneur, puis être tué. Une JVM (machine virtuelle Java) moderne détecte par défaut la limite du cgroup. Toutefois, les builds HotSpot tels qu’OpenJDK n’allouent alors que 25 percent de cette limite au heap. -XX:MaxRAMPercentage=75 est donc généralement le moyen de permettre au heap d’utiliser une plus grande partie de la limite tout en conservant de la mémoire pour les éléments situés en dehors du heap. Depuis la version 12, Node.js dimensionne son heap par défaut à partir de la limite du cgroup. Il utilise environ la moitié de la limite du conteneur, avec un plafonnement autour de 2GB. Définissez donc --max-old-space-size en megabytes uniquement si vous voulez un plafond différent. Gardez cette valeur sous la limite du conteneur afin que le garbage collector intervienne avant le kernel. Avec Ollama, le principe est identique, mais le paramètre est différent, car augmenter num_ctx agrandit le cache KV de centaines de megabytes et le conteneur meurt au milieu d’un prompt long. 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 0 lorsqu’aucune limite n’a été appliquée. Les clés de deploy conçues pour Swarm sont celles du cluster, notamment placement, update_config et endpoint_mode.
Que signifie le code de sortie 137 dans Docker Compose ?
Il signifie presque toujours que le processus principal a reçu SIGKILL, car 137 correspond à 128 plus le signal 9. Un programme peut aussi quitter volontairement avec le code 137, mais c’est rare. L’OOM killer du kernel est la cause la plus fréquente. Un délai d’arrêt expiré produit toutefois 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 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. La Compose Specification exige que les deux valeurs soient cohérentes lorsqu’elles sont toutes les deux définies. Choisissez donc une seule forme et vérifiez le résultat avec docker inspect.
Pourquoi mon conteneur atteint-il sa limite mémoire sans être tué ?
La valeur d’utilisation affichée par docker stats inclut encore les pages de cache récemment utilisées. Le kernel les 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. Il n’est pas 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 la valeur anon maximale de chaque conteneur en conditions réelles avant de fixer les limites. Considérez le total comme un budget, et non comme une capacité à remplir.