Docker Compose en Production : Rotation des Logs, Healthchecks et Limites
Faire tourner des applications multi-conteneurs de manière fiable sur un serveur de production.
1. Éviter la saturation du disque grâce à la rotation des logs
Le driver de logging json-file par défaut de Docker écrit chaque ligne de stdout/stderr de chaque conteneur dans un fichier non plafonné sur le disque de l’hôte. Sur un service qui journalise à un rythme même modéré — logs de requêtes, logs de requêtes ORM, un endpoint de health-check bavard interrogé toutes les quelques secondes — ce fichier grossit sans limite. C’est l’une des causes les plus fréquentes de saturation inattendue du disque d’un VPS en production, et contrairement à une fuite mémoire lente, on peut passer d’un état correct à un disque plein à 100 % en quelques heures dès qu’un service se met à boucler sur des erreurs.
La solution consiste à plafonner la taille et la rotation des logs au niveau du daemon Docker, pas seulement par service. Définir max-size et max-file dans la configuration de logging par défaut du daemon applique la limite à tout conteneur qui ne la surcharge pas explicitement, ce qui signifie qu’un nouveau service ajouté plus tard à la stack hérite automatiquement de valeurs sûres au lieu de réintroduire silencieusement le problème.
Les surcharges par service dans docker-compose.yml restent utiles pour les services dont le volume de logs attendu diffère réellement — le journal d’erreurs d’un conteneur de base de données a rarement besoin de plus de quelques mégaoctets de rétention, tandis qu’un serveur d’application en cours de débogage actif peut en justifier davantage. Définissez les options de logging explicitement par service plutôt que de vous reposer uniquement sur les valeurs par défaut du daemon lorsque les volumes diffèrent sensiblement au sein de votre stack.
2. Configuration de sondes de santé fiables (Healthchecks)
Un conteneur en cours d’exécution n’est pas la même chose qu’un conteneur en bonne santé. Le comportement par défaut de Docker, sans healthcheck, indique seulement qu’un processus ne s’est pas terminé, ce qui manque le mode de défaillance bien plus courant d’un processus Node.js ou Next.js vivant mais bloqué, en attente d’une connexion au pool de base de données, ou renvoyant des 500 à chaque requête parce qu’une dépendance en aval est tombée. Un healthcheck pertinent exerce le véritable chemin de requête, pas une simple connexion TCP.
Pour une application avec une couche API, le healthcheck le plus solide interroge un endpoint dédié /api/health qui vérifie lui-même la connexion à la base de données et toute dépendance critique en aval (Redis, une API externe) avec un timeout court, plutôt que de retourner 200 sans condition. Cela transforme « le processus tourne » en « le processus peut réellement servir du trafic », ce qui est la propriété qui intéresse vraiment votre pipeline de déploiement et votre orchestrateur.
Ajustez interval, timeout, retries et start_period délibérément plutôt que d’accepter les valeurs par défaut. start_period compte énormément pour Next.js et les frameworks similaires qui peuvent prendre plusieurs secondes à terminer leur phase de démarrage (vérifications de génération du client Prisma, préchauffage de cache) — sans un start_period généreux, Docker marque le conteneur unhealthy et votre script de déploiement le tue avant même qu’il ait eu une chance équitable de devenir prêt, créant une boucle de crash auto-infligée.
3. Contrôler la mémoire et le processeur
Sans limites de ressources explicites, un seul conteneur avec une fuite mémoire ou une requête incontrôlée peut consommer toute la RAM disponible de l’hôte, déclenchant l’OOM killer de Linux, qui tue alors des processus de façon assez arbitraire — emportant souvent PostgreSQL ou un autre conteneur sans rapport plutôt que le véritable coupable. Définir un mem_limit strict sur chaque service transforme un incident imprévisible à l’échelle de l’hôte en un simple redémarrage contenu d’un seul conteneur, que la politique de restart de Compose gère automatiquement.
Dimensionner correctement ces limites suppose de connaître l’empreinte mémoire réelle de votre application sous charge, pas de deviner des chiffres ronds. Commencez par observer l’usage réel avec docker stats sur une période de trafic représentative, puis fixez la limite à environ 1,5 à 2 fois le pic observé — assez serré pour détecter de véritables fuites, assez souple pour survivre aux pics de trafic normaux et aux dents de scie du garbage collector sans déclencher de kills injustifiés.
Les limites CPU (cpus dans deploy.resources de Compose, ou le cpu_quota historique) importent moins pour l’isolation que pour l’équité sur un hôte partagé faisant tourner plusieurs services — un job batch ou un worker en arrière-plan ne devrait pas pouvoir affamer votre API en temps CPU lors d’un pic de trafic. Réservez les limites CPU aux hôtes réellement contentieux ; sur un VPS dédié à une seule application, elles ajoutent de la complexité pour peu de bénéfice.
4. Gestion propre des signaux d’arrêt (SIGTERM)
Docker envoie SIGTERM au processus principal d’un conteneur lors d’un arrêt, attend une période de grâce (10 secondes par défaut, configurable via stop_grace_period), puis envoie SIGKILL si le processus n’a pas quitté entre-temps. Si votre application ne gère pas SIGTERM, chaque déploiement ou redémarrage équivaut à un kill brutal des requêtes en cours — une requête à 200 ms de se terminer est coupée en plein milieu de sa réponse, et tout état en mémoire ou transaction de base de données ouverte est perdu sans propreté.
Les processus Node.js ont besoin d’un gestionnaire de signal explicite pour drainer les connexions en cours avant de quitter : arrêter immédiatement d’accepter de nouvelles connexions, laisser les connexions existantes se terminer dans une fenêtre bornée, fermer proprement le pool de base de données, puis quitter. Sans cela, votre pipeline de déploiement sans coupure ne l’est pas vraiment — il déplace simplement la perturbation d’une « coupure visible » vers des « requêtes silencieusement perdues à chaque redémarrage ».
Il existe un second piège moins évident : si l’entrypoint de votre image est un script shell (un motif courant pour exécuter des migrations avant de démarrer l’application), c’est le processus shell — pas votre processus Node.js — qui reçoit le SIGTERM, et il ne le transmet généralement pas à son enfant. Utilisez exec dans les entrypoints shell, ou la gestion native du signal PID 1 de Docker via init: true dans Compose, afin que le signal atteigne réellement le processus capable de le gérer.
5. Politiques de redémarrage et ordre des dépendances
restart: unless-stopped de Compose est le bon choix par défaut pour presque tous les services de production — il survit aux redémarrages de l’hôte et récupère automatiquement après un crash, tout en respectant un docker compose stop délibéré. Évitez restart: always pour les services que vous pourriez avoir besoin d’arrêter volontairement pour maintenance, car il peut, dans certains cas limites selon la version de Docker, redémarrer même après un arrêt explicite ; unless-stopped évite entièrement cette ambiguïté.
depends_on avec un simple nom de service ne contrôle que l’ordre de démarrage, pas l’état de disponibilité — votre conteneur d’application peut démarrer avant que Postgres n’accepte réellement des connexions, provoquant une rafale d’erreurs de connexion à chaque déploiement frais. Utilisez la forme condition: service_healthy accompagnée d’un healthcheck sur la dépendance, ce qui force Compose à réellement attendre que la base de données se déclare en bonne santé avant de démarrer le service dépendant, éliminant toute une classe de conditions de course au démarrage à froid.
6. Synthèse : une stack Compose durcie
Faire tourner Docker Compose de manière fiable en production tient moins à un réglage unique qu’à la fermeture de chaque mode de défaillance silencieux qui ne se manifeste que sous trafic réel : des logs non plafonnés qui remplissent le disque des semaines après le lancement, des healthchecks qui mentent sur la disponibilité réelle, des fuites mémoire qui font tomber des conteneurs sans rapport, et des signaux d’arrêt avalés par un entrypoint shell. Chacun de ces problèmes est invisible en développement local et ne mord qu’une fois le service en charge réelle pendant des jours ou des semaines.
Traitez les quatre piliers — rotation des logs, healthchecks profonds, limites de ressources et arrêt propre — comme des valeurs par défaut non négociables que vous appliquez à chaque définition de service dès le premier jour, et non comme des correctifs ajoutés après un incident. Concrètement : plafonnez les logs json-file au niveau du daemon, écrivez des healthchecks qui vérifient une véritable dépendance, dimensionnez mem_limit à partir de données docker stats observées plutôt que par estimation, et assurez-vous que l’entrypoint de votre application transmette réellement SIGTERM à un gestionnaire qui draine les connexions.
Le bénéfice se cumule une fois que vous superposez des déploiements automatisés (couverts ensuite dans cette série) à une stack Compose durcie : un pipeline de déploiement qui récupère une nouvelle image et échange les conteneurs n’est aussi sûr que les healthchecks et le comportement d’arrêt qui se trouvent en dessous. Posez d’abord correctement cette fondation, et les déploiements sans coupure deviennent un problème d’orchestration simple plutôt qu’un combat contre des conteneurs qui mentent sur leur propre état.

