Plus d'une centaine de conteneurs Docker : notre routine de contrôle mensuel
Gérer plus d'une centaine de conteneurs Docker en production n'est pas inhabituel si vous avez constitué vos services progressivement sur plusieurs années. Un stack Supabase auto-hébergé représente à lui seul treize conteneurs. Ajoutez un frontend web, plusieurs services API, des workers en arrière-plan, un stack de monitoring, l'agrégation des logs, et le nombre grimpe plus vite que la plupart des plans ne l'anticipent. Ce qui devient difficile, ce n'est pas de déployer ces conteneurs — c'est de les maintenir.
La plupart des documentations couvrent le lancement des conteneurs. Très peu traitent de ce qui se passe six mois plus tard, quand le disque se remplit un dimanche, ou quand vous découvrez qu'un tiers de vos conteneurs n'ont pas de politique de redémarrage, ou quand un certificat SSL a expiré discrètement parce que l'alerte de monitoring était coupée. Cet article documente notre routine mensuelle pour un serveur de production hébergeant plus d'une centaine de conteneurs.
Commencer par le disque
Le disque est le mode de défaillance aiguë le plus courant dans les environnements Docker à long terme. Docker accumule des données de façon invisible aux outils système standard. Exécuter df -h affiche l'utilisation du système de fichiers, mais ne vous dit pas que Docker retient cinquante gigaoctets de couches de conteneurs arrêtés, d'images orphelines et de cache de build accumulé depuis six mois de déploiements itératifs.
Le bon point de départ est docker system df, qui détaille l'utilisation du disque par images, conteneurs, volumes locaux et cache de build. La sortie est souvent surprenante. Nous avons vu des serveurs où le cache de build seul dépassait toutes les couches de conteneurs actifs combinées — accumulé silencieusement depuis des mois de builds déclenchés par CI qui n'ont jamais été nettoyés.
Mais en réalité, le chiffre qui importe le plus est la colonne reclaimable. Avant de toucher quoi que ce soit, nous établissons une base de référence : quelle quantité d'espace est actuellement récupérable, et quelle est la trajectoire mois par mois. Si l'espace récupérable croît, le calendrier de nettoyage doit devenir plus agressif ou plus fréquent.
Notre séquence de nettoyage s'exécute dans cet ordre. D'abord, les volumes qui ne sont plus attachés à aucun conteneur :
docker volume prune -f
Ensuite les images orphelines — les couches sans étiquette et non référencées par aucun conteneur actif ou arrêté :
docker image prune -f
Et enfin, si nous avons confirmé qu'une reconstruction complète est faisable dans notre fenêtre de reprise, toutes les images inutilisées de plus de sept jours :
docker image prune -a --filter "until=168h" -f
L'option -a supprime toutes les images inutilisées, pas seulement les orphelines. Nous ne l'exécutons qu'après avoir vérifié que tous les services peuvent être reconstruits depuis le registre dans notre délai de reprise acceptable. Cette vérification a lieu avant la commande, pas après.
Politiques de redémarrage
Les politiques de redémarrage déterminent ce qui se passe lorsqu'un conteneur s'arrête inopinément ou lorsque le daemon Docker redémarre après un reboot de l'hôte. La plupart des guides de déploiement mentionnent cela brièvement. Mais en réalité, une politique de redémarrage mal configurée est ce qui vous amène à découvrir des services silencieusement arrêtés depuis deux semaines — que personne n'a remarqué parce que l'alerte de monitoring pointait vers le mauvais endpoint.
Docker propose quatre politiques de redémarrage. no est la valeur par défaut : le conteneur ne redémarre en aucune circonstance. always redémarre le conteneur chaque fois qu'il s'arrête, y compris lors du redémarrage du daemon, quel que soit le code de sortie. unless-stopped se comporte comme always mais respecte les arrêts explicites — si vous exécutez docker stop avant un reboot, le conteneur reste arrêté après le reboot. on-failure[:max-retries] ne redémarre que sur des codes de sortie non nuls, avec une limite de tentatives optionnelle avant d'abandonner.
Pour les services web sans état et les workers API, nous utilisons unless-stopped. Si nous arrêtons délibérément un conteneur pendant une fenêtre de maintenance, il doit rester arrêté après le prochain reboot plutôt que de revenir inopinément. always le redémarrerait quelle que soit la raison de son arrêt.
Pour les conteneurs de migration de base de données ou les jobs d'initialisation uniques, la politique correcte est no. Une migration qui échoue ne doit pas boucler. on-failure:3 convient aux conteneurs qui doivent réessayer brièvement contre une dépendance temporairement indisponible — un consommateur de file d'attente externe attendant qu'un broker devienne accessible, par exemple — mais qui ne doivent pas s'exécuter indéfiniment.
Notre contrôle mensuel exécute une seule commande sur tous les conteneurs :
docker inspect --format '{{.Name}} {{.HostConfig.RestartPolicy.Name}}' $(docker ps -aq)
Tout conteneur avec la politique no qui n'est pas intentionnellement un job à usage unique est examiné. Dans la plupart des cas, cela signifie qu'un service a été démarré avec docker run lors d'un incident et n'a jamais été formellement ajouté à la configuration compose avec une politique de redémarrage appropriée.
Rotation des logs
Le driver de logging Docker par défaut est json-file. Par défaut, il n'impose aucune limite de taille. Un conteneur émettant un flux modeste de lignes de log peut produire des centaines de gigaoctets sur plusieurs mois. Ce n'est pas une préoccupation théorique — c'est l'une des causes les plus courantes d'épuisement du disque sur des serveurs de production configurés sans gestion délibérée des logs.
La solution est une politique globale dans /etc/docker/daemon.json :
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "5"
}
}
Cela plafonne les logs de chaque conteneur à cinq fichiers de cent mégaoctets chacun — cinq cents mégaoctets maximum par conteneur. Le daemon Docker doit être redémarré après ce changement, et surtout, les conteneurs doivent être recréés, pas seulement redémarrés, pour que les nouveaux paramètres de log prennent effet.
Mais en réalité, le paramètre daemon.json ne s'applique qu'aux conteneurs créés après le changement. Les conteneurs existants conservent indéfiniment leur configuration de log d'origine. C'est l'erreur la plus courante que nous rencontrons : la politique est définie, le daemon est redémarré, et l'hypothèse est que tous les conteneurs sont désormais conformes. Ce n'est pas le cas. Notre contrôle mensuel vérifie la configuration des logs par conteneur :
docker inspect --format '{{.Name}} {{.HostConfig.LogConfig}}' $(docker ps -q)
Les conteneurs sans limites de taille explicites sont recréés avec la configuration mise à jour lors de la prochaine fenêtre de maintenance. L'ordre de recréation est important — les services avec état ont besoin que leurs volumes de données restent en place, et les services dépendants doivent démarrer dans le bon ordre.
Expiration des certificats SSL
Les certificats SSL expirent. Le monitoring automatisé attrape la plupart des cas. Mais le monitoring automatisé est aussi mal configuré, génère de la fatigue aux alertes, ou tombe silencieusement en même temps que le service qu'il est censé surveiller. Notre routine mensuelle inclut une vérification manuelle indépendante de tout système automatisé.
Pour chaque domaine public, nous vérifions directement le certificat :
echo | openssl s_client -connect domain.com:443 -servername domain.com 2>/dev/null | openssl x509 -noout -enddate
Cela affiche la date notAfter. Tout ce qui expire dans les trente jours entre immédiatement dans la file de renouvellement, indépendamment de ce qu'indique tout tableau de bord de monitoring. La vérification manuelle est le filet de sécurité.
Pour l'infrastructure de certificats auto-gérée — que nous exploitons pour plusieurs services internes — nous vérifions les certificats intermédiaires séparément des certificats feuilles. Un intermédiaire expiré provoque l'échec de validation complète de la chaîne même lorsque le certificat feuille lui-même est encore valide. Ce mode d'échec est moins visible qu'un certificat feuille expiré : les navigateurs et les clients peuvent signaler des erreurs confuses plutôt que le message clair « certificat expiré » que la plupart des ingénieurs attendent.
Nous maintenons un script shell qui itère sur une liste de domaines, extrait la date d'expiration via openssl, et affiche un avertissement pour tout ce qui expire dans les trente jours et une alerte critique pour tout ce qui expire dans les sept jours. Ce script s'exécute comme une tâche cron, mais nous le lançons aussi manuellement lors de la revue mensuelle comme confirmation secondaire que la sortie cron a été correcte. Les tâches cron échouent silencieusement plus souvent que la plupart des gens ne le pensent.
Limites de ressources des conteneurs
Sans limites de mémoire, un conteneur défaillant peut épuiser la RAM de l'hôte et déclencher le OOM killer du noyau sur des processus sans rapport. Sans limites CPU, un processus incontrôlable peut affamer les conteneurs voisins assez longtemps pour provoquer des défaillances en cascade. Aucun de ces cas n'est un cas limite rare.
La revue mensuelle vérifie les limites de ressources sur tous les conteneurs actifs :
docker stats --no-stream --format "table {{.Name}} {{.CPUPerc}} {{.MemUsage}} {{.MemLimit}}"
Les conteneurs affichant 0B / 0B dans la colonne de limite mémoire n'ont aucune contrainte définie. Nous examinons chacun et déterminons une limite appropriée. Pour les services HTTP sans état, une limite mémoire de deux à quatre fois l'ensemble de travail observé est un point de départ raisonnable. L'objectif n'est pas d'être précis — c'est d'empêcher une croissance illimitée de faire tomber les services co-localisés.
Nous regardons également le pourcentage CPU lors de la passe de stats. Un conteneur constamment proche de cent pourcent CPU sur un hôte multi-cœurs suggère soit un processus incontrôlable, soit un conteneur sous-dimensionné pour sa charge de travail. Les deux conditions méritent investigation avant le mois suivant.
Vérification des mises à jour d'images
Les images de base reçoivent des correctifs de sécurité selon des calendriers irréguliers. Un conteneur exécutant une image qui était à jour il y a six mois peut fonctionner avec une version de nginx ou PostgreSQL présentant des vulnérabilités connues. Nous ne tirons et redéployons pas automatiquement chaque conteneur chaque mois — cela crée plus de risques que cela n'en atténue. Mais nous vérifions bien ce qui tourne par rapport à ce qui est actuel.
L'approche pratique : pour chaque service avec une version d'image fixée, nous vérifions la version fixée par rapport au changelog en amont une fois par mois. Pour les services utilisant un tag flottant comme latest ou 16-alpine, nous tirons et comparons le digest de l'image pour déterminer si quelque chose a changé. Si c'est le cas, nous examinons ce qui a changé avant de déployer.
Mais en réalité, la discipline la plus importante est de s'éloigner des tags flottants. Un service qui a été silencieusement redéployé avec un changement cassant parce que son tag latest pointait vers une nouvelle version majeure est un problème plus difficile à diagnostiquer qu'un service qui exécute une image ancienne connue. Fixez les versions, puis mettez-les à jour délibérément.
La discipline
Pris ensemble, le contrôle mensuel couvre six domaines : utilisation et nettoyage du disque, vérification des politiques de redémarrage, paramètres de rotation des logs, expiration des certificats SSL, limites de ressources des conteneurs et actualité des images. Aucune de ces tâches ne nécessite plus de quatre-vingt-dix minutes au total sur un serveur bien documenté. La valeur ne réside pas dans les vérifications individuelles — elle réside dans leur exécution selon un calendrier fixe, avant qu'une panne survienne plutôt qu'après.
Les systèmes de production se dégradent progressivement. Le disque s'accumule. Les logs grossissent. Les certificats vieillissent. Les images prennent du retard. Aucun de ces processus ne génère d'alerte avant que le seuil soit dépassé. Le contrôle mensuel déplace la charge de maintenance du réactif vers le prévisible, ce qui est une posture opérationnelle entièrement différente.
Ressources connexes
- Building an Amazon Data Warehouse with FastAPI and TimescaleDB — la même discipline d'infrastructure appliquée à la couche données
- Solo Operations at Scale: Managing Dozens of Projects with a Small Team — comment nous structurons la maintenance récurrente à travers un large portefeuille de projets
Articles connexes
Quand l'authentification Docker casse la connexion mobile : une histoire de bug multi-plateforme
Déployer des applications React en production : configuration Docker complète avec le reverse proxy Traefik
Construire une stack de développement multi-tenant avec Docker : configuration complète pour des déploiements clients évolutifs