Introduction
Compose simplifie la vie des équipes multi‑services, mais il est facile de livrer une pile lente si l'on accepte tous les réglages par défaut. Les goulets d'étranglement fréquents sont bien connus :
- I/O fichier : les bind mounts sur macOS et Windows sont bien plus lents que les volumes natifs Linux. Les bases de données en souffrent particulièrement.
- CPU et mémoire : des limites trop basses ou des voisins bruyants provoquent du throttling et du swap.
- Réseau : des sauts supplémentaires et le NAT ajoutent de la latence ; les résolutions DNS et des health checks trop bavards chargent la pile.
- Logs : des journaux non bornés grossissent et volent de l'I/O au travail utile.
- Conteneurs très écrits : caches et répertoires temporaires dégradent le débit s'ils ne sont pas déplacés en tmpfs.
Ce guide montre un chemin sûr, par étapes, pour trouver les goulots d'étranglement, dimensionner correctement les ressources et régler la latence comme le débit. Vous verrez des extraits Compose concrets pour Nginx, PostgreSQL et Redis, ainsi que des vérifications rapides à exécuter en local avant tout changement dans des environnements partagés. L'objectif est une optimisation des performances Docker Compose pragmatique et mesurable.
Vue d'ensemble du workflow
Adoptez un flux court et reproductible afin que chaque amélioration soit évidente et sûre.
- Définir les objectifs
- SLO de latence : p50, p95, p99 pour vos endpoints et commandes clés.
- Cibles de débit : requêtes/s, TPS, ops/sec.
- Plafonds de ressources : CPU et mémoire max par service.
- Établir une baseline rapide
- Démarrez la pile et attendez l'état « healthy ».
- Enregistrez un cliché de 1-2 minutes CPU/Mémoire/I/O avec
docker stats. - Mesurez la latence HTTP :
curl -s -o /dev/null -w "time_connect=%{time_connect} time_starttransfer=%{time_starttransfer} time_total=%{time_total}\n" http://localhost:8080/- Mesurez des opérations simples DB/cache : un
SELECTtrivial dans PostgreSQL et unGET/SETdans Redis.
- Identifier les contraintes
- Points chauds I/O : services avec écritures élevées sur bind mounts.
- Saturation CPU : conteneurs proches de 100 % d'un cœur ou souvent throttlés.
- Pression mémoire : OOM kills, reclaim élevé ; buffers partagés DB trop petits.
- Réseau : temps de connexion lents, délais DNS, backlog SYN élevé.
- Logs : débit de logs élevé ou fichiers volumineux.
- Appliquer un seul changement à la fois
- Préférez les volumes nommés pour les bases. Gardez les bind mounts avec
cached/delegatedpour le code/config à éditer à chaud. - Ajoutez du
tmpfspour les caches et répertoires temporaires chauds. - Fixez des limites CPU/mémoire conservatrices, puis augmentez progressivement.
- Bornage et rotation des logs avec le driver
local. - Ajustez les health checks pour réduire le bruit sans sacrifier la sécurité.
- Re‑tester et comparer
- Rejouez les mêmes commandes
curlet charges. Conservez fenêtre, concurrence et dataset identiques. - Si les chiffres s'améliorent sans hausse d'erreurs, gardez le changement ; sinon, revenez en arrière.
Plan pilote local
Commencez par un pilote restreint facile à inspecter en local. L'exemple ci‑dessous s'appuie sur Nginx, PostgreSQL et Redis, et cible l'I/O fichier, les plafonds de ressources, la journalisation et les health checks. Adaptez l'upstream web et les identifiants DB à votre pile.
Baseline compose.yml
version: "3.9"
services:
web:
image: nginx:1.25-alpine
ports:
- "8080:80"
volumes:
# Config en bind mount en lecture seule pour la commodité
- ./nginx.conf:/etc/nginx/nginx.conf: ro
# Cache sur volume nommé pour éviter la lenteur des bind mounts
- nginx_cache:/var/cache/nginx
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost"]
interval: 10s
timeout: 2s
retries: 3
# Journaux bornés et légers
logging:
driver: local
options:
max-size: "10m"
max-file: "3"
# Limites conservatrices (prises en charge par docker compose sous Linux)
cpus: "1.5"
mem_limit: "512m"
# Plus de descripteurs pour des tests à haute connexion
ulimits:
nofile: 65536
# Réduit les pertes de file d'attente sous charge
sysctls:
net.core.somaxconn: "65535"
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: example
POSTGRES_DB: app
POSTGRES_USER: app
# Volume nommé pour le stockage durable (plus rapide que bind mount sur macOS/Windows)
volumes:
- pgdata:/var/lib/postgresql/data
# Plus de mémoire partagée pour réduire la contention IPC
shm_size: "512m"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 10s
timeout: 3s
retries: 5
logging:
driver: local
options:
max-size: "10m"
max-file: "3"
cpus: "2"
mem_limit: "1g"
cache:
image: redis:7-alpine
command: [
"redis-server",
"--appendonly", "no",
"--maxmemory", "256mb",
"--maxmemory-policy", "allkeys-lru"
]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 2s
retries: 5
logging:
driver: local
options:
max-size: "10m"
max-file: "3"
cpus: "1"
mem_limit: "384m"
# File d'acceptation plus rapide sous charge en rafale
sysctls:
net.core.somaxconn: "65535"
volumes:
pgdata: {}
nginx_cache: {}
Notes
- Utilisez des volumes nommés pour Postgres et le cache Nginx. Cela évite la lenteur des bind mounts sur macOS/Windows et reste performant sous Linux.
- Laissez la config en bind mount lecture seule pour la commodité, pas pour l'I/O à haut débit.
- Les paramètres
cpusetmem_limitsont des points de départ conservateurs ; adaptez‑les à votre hôte.
Réglage optionnel des bind mounts pour macOS/Windows
Si vous devez absolument bind‑monter un chemin très écrit (déconseillé pour les bases), ajoutez des drapeaux de cohérence :
services:
app:
volumes:
- ./src:/app/src: cached # lecture majoritaire
- ./tmp:/app/tmp: delegated # écriture majoritaire
Réseau host Linux pour des benchs
Pour des expériences de latence sous Linux, vous pouvez éviter un saut NAT en utilisant le réseau host pour l'entrée HTTP :
services:
web:
network_mode: host # Linux uniquement
ports: [] # inutile avec le réseau host
N'utilisez pas le réseau host sur macOS/Windows, et évitez‑le pour le dev général à moins d'en avoir besoin pour du benchmarking.
Ajouter du tmpfs pour les écritures chaudes
Réduisez l'I/O disque en déplaçant les répertoires temporaires/caches en RAM :
services:
web:
tmpfs:
- /var/cache/nginx
cache:
tmpfs:
- /tmp
Vérifications rapides de latence et de débit
Lancez‑les après que la pile soit healthy pour capturer une baseline :
- Latence HTTP (p50 sur petit échantillon) :
curl -s -o /dev/null -w "time_connect=%{time_connect} time_starttransfer=%{time_starttransfer} time_total=%{time_total}\n" http://localhost:8080/- Débit HTTP (si vous avez hey) :
hey -z 30s -c 50 http://localhost:8080/- Vérification rapide PostgreSQL :
psql "postgresql://app: example@localhost:5432/app" -c "select 1;"- Si vous avez pgbench :
pgbench -S -T 30 -c 16 -j 4 "postgresql://app: example@localhost:5432/app" - Vérification rapide Redis :
redis-cli -h localhost -p 6379 ping- Si vous avez redis-benchmark :
redis-benchmark -t get, set -n 100000 -q
Répétez exactement les mêmes commandes après chaque changement. Conservez durée, concurrence et dataset constants pour rendre les comparaisons fiables.
Recettes d'optimisation pratiques
- Éliminer les bind mounts lents là où ça fait mal
- Volumes nommés pour les bases de données et caches de service.
- Conservez code source et configs en bind mount si l'édition à chaud est utile.
- Sur macOS/Windows, préférez
:cachedpour la lecture majoritaire et:delegatedpour l'écriture majoritaire.
- Dimensionner correctement CPU et mémoire
- Démarrez avec
cpusetmem_limitconservateurs et augmentez jusqu'à disparition du throttling et stabilisation de la latence. - Isolez des cœurs si nécessaire pour éviter les voisins bruyants :
services:
db:
cpuset: "1-2"
- Maîtriser la surcharge de logs
- Utilisez le driver
localavec taille et rotation bornées. Évitezjson-filenon borné.
- Régler les health checks
- Indispensables pour la dispo, mais peuvent charger la pile. Utilisez une sonde simple/rapide, un intervalle de 10-30 s, et un petit timeout.
- Déplacer les temporaires chauds en tmpfs
- Idéal pour caches et fichiers éphémères qu'on peut perdre sans risque.
- Augmenter certaines files noyau prudemment
- Des taux de connexions élevés bénéficient d'un
somaxconnplus grand.
- Mesurer, ne pas deviner
- Rejouez les mêmes tests avant/après. Prenez des notes de chaque changement et de ses résultats.
Optimisation sûre via overrides et profils
Gardez votre compose.yml de base simple. Placez les réglages de tuning dans un override pour activer à la demande :
# compose.override.yml
services:
web:
tmpfs:
- /var/cache/nginx
environment:
NGINX_ENV: bench
db:
mem_limit: "2g"
cache:
command: [
"redis-server",
"--appendonly", "no",
"--save", "",
"--maxmemory", "512mb",
"--maxmemory-policy", "allkeys-lru"
]
Ou utilisez des profils pour des réglages réservés aux benchs :
services:
web:
profiles: ["bench"]
network_mode: host # Linux uniquement
ports: []
Activation :
COMPOSE_PROFILES=bench docker compose up -d
Checklist de dépannage
- Pics de latence après quelques minutes : vérifiez les logs (rotation), augmentez le
tmpfsdes caches, confirmez l'absence d'OOM kills. - Écritures lentes Postgres : assurez‑vous que les données sont sur un volume nommé, augmentez
shm_size, gardez le WAL sur le même volume nommé. - Timeouts Redis sous rafale : augmentez
somaxconnetnofile, envisagez le réseau host sous Linux pour les tests, vérifiez la politiquemaxmemory. - Throttling CPU : augmentez
cpusou réduisez la concurrence ; vérifiez l'isolementcpuset. - I/O fichier macOS/Windows : remplacez les bind mounts par des volumes nommés sur les chemins chauds ; appliquez
:cached/:delegatedseulement en dernier recours.
Conclusion
Gardez une boucle serrée : définissez vos objectifs, établissez une baseline, changez un seul paramètre, remesurez et journalisez les résultats. Commencez avec des réglages sûrs, préférez les volumes nommés pour l'I/O chaud, bornez vos journaux et utilisez tmpfs pour les écritures éphémères. Une fois des gains nets observés en local, capturez‑les dans des overrides ou des profils pour une adoption d'équipe cohérente. Revalidez régulièrement vos chiffres au fil des mises à jour d'images, des versions d'OS hôte et de l'évolution des charges pour que vos stacks restent rapides et prévisibles, avec une Docker Compose latency maîtrisée et des Docker Compose bottlenecks sous contrôle.