Introduction
Si vos conteneurs fonctionnent bien en local mais plantent ou sont limités sous une vraie charge, vous avez probablement besoin de limites explicites CPU et mémoire ainsi que d'une méthode reproductible pour les valider. Ce guide montre comment :
- Définir des limites CPU et mémoire avec
docker runet Docker Compose - Détecter et diagnostiquer les conteneurs
OOMKilled - Lire
docker statspour comparer l'utilisation réelle aux limites configurées - Exécuter un pilote local sûr avant un déploiement plus large
- Éviter les pièges courants en production et les erreurs de planification de capacité
L'objectif est un flux de travail simple qui donne un comportement proche de la production sur un portable ou un hôte unique, sans surprises ultérieures.
Aperçu du workflow
Utilisez cette boucle pour chaque service.
- Définir un budget modeste et des critères de réussite
- Choisissez une limite mémoire initiale (par exemple 256 Mio) et un budget CPU (par exemple 0,5 CPU).
- Définissez succès/échec : le service reste sain pendant N minutes sous une charge définie, latence p95 dans la cible, aucun
OOMKilled, throttling CPU acceptable pour votre SLO.
- Appliquer des limites pour des tests rapides (
docker run)
Limite mémoire dure et politique de swap :
docker run --rm \
--name web \
--memory=256m \
--memory-swap=256m \
-p 8080:80 nginx:alpine
Notes :
--memorydéfinit la limite RAM dure.--memory-swapdéfinit RAM + swap. Mettez-le égal à--memorypour désactiver le swap pour le conteneur. Mettez-1pour autoriser un swap illimité (si le swap hôte est activé).
Limite CPU et pinning :
# Limiter à 0,5 CPU (part d'un cœur dans le temps)
docker run --rm --cpus=0.5 nginx:alpine
# Optionnellement épingler aux cœurs 0 et 1
docker run --rm --cpuset-cpus="0,1" --cpus=1.0 nginx:alpine
- Observer l'utilisation avec
docker stats
docker stats
Champs clés :
- CPU % : relatif aux CPUs assignés. Un conteneur avec
--cpus=1.0à 100 % est saturé. - MEM USAGE / LIMIT et MEM % : mémoire courante et le plafond configuré.
- PIDS : nombre de processus/threads dans le conteneur.
- Détecter et diagnostiquer
OOMKilled
Symptômes :
- Le conteneur s'arrête inopinément, souvent avec le code de sortie 137 (tué par SIGKILL).
docker psoudocker inspectafficheOOMKilled=true.
Commandes :
# Était-ce un OOMKilled ?
docker inspect -f '{{.State.OOMKilled}}' <container_id>
# Quel était le code de sortie ?
docker inspect -f '{{.State.ExitCode}}' <container_id>
# Événements récents du démon (chercher oom)
docker events --since 10m | grep -i oom || true
Pour reproduire intentionnellement à des fins d'apprentissage :
# Allouer trop de mémoire pour déclencher OOMKilled
docker run --rm --memory=128m --memory-swap=128m python:3.11-alpine \
python -c "a=['x'*10_000_000 for _ in range(100)]; import time; time.sleep(60)"
Si vous voyez OOMKilled, augmentez la limite, réduisez la charge ou plafonnez le tas du runtime (voir exemples Dockerfile ci-dessous).
- Encoder les limites dans Docker Compose
Deux approches courantes selon la cible.
docker compose local (non Swarm) : Privilégiez les clés de ressources de la version 2.x du fichier Compose pour une application locale.
version: "2.4"
services:
api:
image: myorg/api:1.0.0
mem_limit: 512m
mem_reservation: 128m
cpus: "1.0"
cpu_shares: 512
ports:
- "8080:8080"
Notes :
mem_limitetcpussont respectés localement avec les fichiers v2.x.cpu_sharesdéfinit le poids relatif quand l'hôte est occupé ; ce n'est pas un plafond dur par lui-même.
Déploiements Swarm : Utilisez deploy.resources (Compose v3+). Ceux-ci sont pour le mode Swarm.
version: "3.8"
services:
api:
image: myorg/api:1.0.0
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
ports:
- "8080:8080"
Notes :
deploy.resourcessont appliqués par Swarm. Ne comptez pas sur eux pour dudocker composelocal pur sauf si vous validez aussi l'application effective.
- Ajouter des plafonds mémoire au niveau du runtime dans votre Dockerfile
Les limites du conteneur plafonnent l'ensemble de l'arbre de processus. De nombreux runtimes ont aussi besoin de plafonds au niveau application pour ne pas allouer au-delà de votre budget.
Exemples :
Node.js : plafonner le tas V8
FROM node:20-alpine
ENV NODE_OPTIONS=--max-old-space-size=256
Java : définir le tas et l'ergonomie consciente du conteneur
FROM eclipse-temurin:21-jre
ENV JAVA_TOOL_OPTIONS="-XX:+UseContainerSupport -Xms128m -Xmx256m"
Python : réduire les arènes de l'allocateur glibc pour limiter la croissance du RSS
FROM python:3.11-alpine
ENV MALLOC_ARENA_MAX=2
Gardez ces réglages alignés avec votre --memory conteneur. Si le runtime peut dépasser le plafond, vous verrez OOMKilled sous charge.
- Tester la charge localement de manière contrôlée
Vous pouvez générer une pression CPU ou mémoire simple sans outils supplémentaires :
# Brûler CPU (le plafond 0,5 CPU limitera ce processus)
docker run --rm --cpus=0.5 alpine sh -c "yes > /dev/null"
# Pic mémoire (attendre OOMKilled à 128m)
docker run --rm --memory=128m --memory-swap=128m alpine \
sh -c "python3 - <<'PY'
a=['x'*10_000_000 for _ in range(100)]
import time; time.sleep(60)
PY"
Surveillez docker stats et vos métriques de service pendant le test. Ajustez les limites et les plafonds runtime jusqu'à ce que vos objectifs tiennent.
- Documenter les budgets et les compromis
Enregistrez pour chaque service :
- Mémoire en état stable, mémoire de pointe et limite
- Budget CPU et saturation observée sous charge
- Quand le throttling est acceptable vs quand passer à l'échelle horizontale
- Modes de défaillance connus (par exemple,
OOMKilledquand la taille de lot > N)
Plan de pilote local
Rendez le premier pilote étroit, mesurable et facile à inspecter localement.
Périmètre
- Un service important mais simple (par exemple, une API publique ou un worker).
Configuration initiale
# Compose pour tests locaux (v2.4)
version: "2.4"
services:
api:
image: myorg/api:1.0.0
mem_limit: 256m
mem_reservation: 128m
cpus: "0.5"
ports: ["8080:8080" ]
Étapes de test
- Démarrer le service et confirmer sa disponibilité.
- Appliquer une charge faible et constante (par exemple, une boucle
curlà 5 req/s pendant 10 minutes). - Observer avec
docker stats. Sauvegarder un instantané aux minutes 1, 5 et 10. - Vérifier les redémarrages ou
OOMKilledavecdocker psetdocker inspect. - Augmenter la charge d'un petit pas (par exemple, +2 req/s) et répéter.
Critères de réussite
- Aucun événement
OOMKilled. - Latence p95 dans la cible.
- MEM % reste sous 80 % en état stable ; de courts pics sont tolérés s'ils ne provoquent pas d'
OOMKilled. - CPU % sous le plafond produit une latence acceptable. Sinon, augmenter
cpusou ajouter des répliques.
Itération suivante
- Si la mémoire est serrée, baisser le tas du runtime ou augmenter
mem_limitmodestement (par exemple, +64m). - Si le CPU est saturé et la latence souffre, augmenter
cpus(par exemple, 0,5 -> 1,0) ou ajouter une deuxième réplique. - Reporter les changements dans Compose et relancer les mêmes étapes pour comparer.
Référence rapide de dépannage
- Le conteneur redémarre avec le code 137 et
OOMKilled=true: réduire l'utilisation mémoire ou relever la limite ; aligner le tas du runtime avec la limite du conteneur ; envisager de désactiver le swap pour le conteneur en mettant--memory-swapégal à--memory. - CPU bloqué à 100 % avec throttling : augmenter
--cpus, réduire le travail par requête, ou ajouter des répliques. Épingler à des cœurs spécifiques seulement si vous avez une raison (isolation, test NUMA). docker statsmontre MEM proche de LIMIT alors que le tas de l'appli semble petit : tenir compte des allocations natives, caches, JIT, threads et page cache ; réduire les arènes d'allocateur ou le cache de code JIT si applicable.- Les ressources Compose ne s'appliquent pas localement : utiliser
mem_limit/cpusv2.x pour les tests locaux ; traiterdeploy.resourcescomme Swarm-only sauf vérification contraire. - Sur macOS/Windows : Docker s'exécute dans une VM. Assurez-vous que la VM a assez de mémoire et de CPUs, sinon tous les conteneurs peuvent se contendre ou être tués au niveau de la VM.
Mises en garde pour la planification de capacité
- Laissez de la marge. Ne fixez pas les limites à l'utilisation de pointe observée ; prévoyez les pics, GC, JIT et tampons noyau.
- Séparez comportement stable et comportement en pointe. Si les pics causent
OOMKilled, ajustez l'application (taille de lot, tas, mise en cache) avant de simplement relever les limites. - Observez dans le temps. Des tests courts peuvent cacher des fuites et de la fragmentation qui n'apparaissent qu'après des heures ou des jours.
- Équilibrez plafonds CPU et objectifs de latence. Les plafonds durs protègent les voisins mais peuvent augmenter la latence de queue sous charge bursty ; choisissez selon vos SLO.
- Documentez les budgets par service pour que de futurs changements n'effacent pas silencieusement vos garanties.
Conclusion
Commencez petit et délibérément : fixez des limites CPU et mémoire conservatrices, vérifiez avec docker stats et l'état de sortie du conteneur, et encodez les réglages dans Compose adaptés à votre environnement. Ajoutez des plafonds mémoire au niveau du runtime pour garder votre application dans le budget du conteneur. Utilisez le plan de pilote local pour étendre la couverture service par service. Avec le temps, vos limites deviennent des valeurs par défaut fiables, proches de la production, que vous pouvez faire évoluer en toute confiance.