Vous avez une petite stack « production » sur Docker Compose. Ça fonctionnait depuis des mois. Puis une mise à jour routinière se transforme en une interruption de 2 minutes,
votre chat se remplit de « est-ce que c’est en panne ? », et vous regardez docker compose up -d comme s’il vous avait personnellement trahi.
Compose peut tout à fait exécuter de vraies charges. Mais « mises à jour sans interruption sur Compose » n’est pas une option à activer — c’est un ensemble de choix opérationnels
que vous implémentez, testez, et parfois regrettez. Séparons le mythe marketing de la réalité technique, puis construisons des modèles
qui tiennent quand des clients sont connectés, des requêtes sont en vol et votre base de données a des sentiments.
Mythe vs réalité : ce que Compose peut et ne peut pas faire
Le mythe : « Compose fait des rolling updates »
Docker Compose, à lui seul, n’est pas un orchestrateur. Il n’effectue pas nativement de rolling updates avec drainage conscient du trafic, découverte de service
entre hôtes, ou remplacement automatique en cas d’échec. Quand vous lancez docker compose up -d, Compose réconcilie l’état désiré sur un seul hôte.
Il peut recréer des conteneurs. Il peut les arrêter et les démarrer. Il ne coordonnera pas « garder l’ancien, démarrer le nouveau, basculer le trafic,
puis retirer l’ancien » sauf si vous construisez ces mécaniques autour.
La réalité : « Compose peut s’approcher du sans interruption avec un proxy inverse et de la discipline »
Vous pouvez atteindre un quasi-zéro interruption pour beaucoup de charges web. L’astuce est d’arrêter de considérer « le conteneur applicatif » comme le détenteur du socket.
Votre point d’entrée stable doit être un proxy inverse (ou proxy L4) qui reste actif pendant que vous échangez les backends derrière lui. Ajoutez ensuite :
- Des checks de santé qui reflètent la readiness, pas seulement la liveness.
- Un arrêt gracieux (signaux d’arrêt et timeouts) pour laisser finir les requêtes en vol.
- Une méthode de déploiement qui démarre de nouvelles instances avant d’arrêter les anciennes.
- Une discipline pour les changements de base de données (expand/contract, compatibilité ascendante, éviter les migrations « stop the world »).
Si votre stack est un seul conteneur écoutant sur le port 443 de l’hôte, vous essayez de faire du « zéro interruption » en arrachant sans cesse les planches du plancher.
C’est possible. C’est aussi un passe-temps étrange.
Une phrase à garder en tête : L’espoir n’est pas une stratégie
— souvent attribuée aux cercles de fiabilité ; prenez-la comme une idée paraphrasée,
pas comme une preuve en justice.
Faits intéressants et contexte historique (pourquoi c’est déroutant)
- Compose a commencé comme « Fig » (2014). Il était conçu pour les flux de travail développeur, pas pour des déploiements en production avec des SLO.
-
Docker a introduit « Swarm mode » plus tard avec des rolling updates, du rescheduling piloté par la santé et des abstractions de service — des fonctionnalités que Compose
n’a jamais reçues. -
depends_onn’a jamais signifié « attendre la readiness ». C’est un ordre de démarrage, pas un contrôle de disponibilité. Cette incompréhension a causé
plus d’interruptions « ça marche sur ma machine » que la plupart n’osent l’admettre. -
Les healthchecks sont arrivés plus tard (ère Docker 1.12). Avant, on utilisait « sleep 10 » comme stratégie de readiness. C’est resté populaire,
pour des raisons en grande partie psychologiques. -
Les politiques de redémarrage ne sont pas de l’orchestration.
restart: alwaysest une ceinture de sécurité, pas un pilote automatique. - Nginx supporte le rechargement gracieux depuis longtemps — une des raisons majeures qui en ont fait la porte d’entrée par défaut pour les déploiements DIY sans interruption.
-
Linux a ajouté
SO_REUSEPORT(usage mainstream autour de 2013) permettant à plusieurs processus de binder le même port dans certains designs,
mais cela ne règle pas magiquement la coordination de déploiement pour des conteneurs. - Le déploiement blue/green est antérieur aux conteneurs. Les équipes ops le faisaient avec des paires de VM et des load balancers bien avant que Docker ne le rende tendance.
- Les migrations de base de données sont la vraie usine à downtime. Les conteneurs applicatifs sont faciles ; les changements de schéma avec verrous exclusifs sont là où les rêves vont mourir.
Définir « zéro interruption » comme un adulte
« Zéro interruption » est une expression qui signifie des choses radicalement différentes selon qui transpire. Définissez clairement l’objectif avant de changer quoi que ce soit.
Voici les versions courantes :
- Pas de coupure d’acceptation TCP : le port n’arrête jamais d’accepter des connexions. Les clients peuvent quand même voir des erreurs si votre backend n’est pas prêt.
- Pas de pic de 5xx : les requêtes continuent de réussir. Une augmentation de latence peut être acceptable.
- Pas d’interruption visible par l’utilisateur : les sessions persistent, les websockets survivent, les long-polls continuent. C’est plus difficile qu’il n’y paraît.
- Pas de consommation du budget d’erreurs induite par le déploiement : le changement peut encore causer des problèmes, mais pas le processus de déploiement lui-même.
Pour des stacks basées sur Compose, l’objectif réaliste est généralement : pas de pic de 5xx et pas de coupure sur le point d’accès public, avec une augmentation
de latence bornée pendant l’échange. Si vous servez des websockets, redéfinissez le succès : vous pouvez être « zéro interruption » et quand même déconnecter des clients à moins
d’implémenter explicitement le drainage des connexions et un routage sticky.
Blague #1 : Si quelqu’un vous dit qu’il a un « vrai zéro interruption » sur Compose avec un seul conteneur, demandez-lui ce qu’il utilise pour voyager dans le temps.
Les modes de panne que vous rencontrerez en production
1) Le binding de port est un point unique de douleur
Si votre conteneur applicatif bind 0.0.0.0:443 sur l’hôte, vous ne pouvez pas démarrer la nouvelle version tant que l’ancienne n’a pas libéré le port.
Cela crée un intervalle. Même si c’est 200 ms, c’est un intervalle. Sous charge, les clients reculent mal, les retries s’enchaînent et soudain « millisecondes » devient
« pourquoi notre checkout a échoué ».
2) « Conteneur démarré » ≠ « service prêt »
Beaucoup d’applications démarrent leur process rapidement, puis passent 5–30 secondes à faire des migrations, remplir des caches ou attendre des dépendances. Si votre proxy
route le trafic trop tôt, vous servirez des erreurs. Si vous bloquez le routage jusqu’à la readiness, vous serez bien — à condition que la readiness soit mesurée correctement.
3) La gestion de SIGTERM n’est pas optionnelle
Docker enverra SIGTERM (par défaut) puis SIGKILL après le timeout d’arrêt. Si votre app ignore SIGTERM, vous laisserez tomber des requêtes en vol.
Si votre stop timeout est trop court, vous laisserez aussi tomber des requêtes en vol. Si vous terminez le proxy en premier, vous laisserez tomber tout.
4) Migrations de base de données qui lockent les tables
L’interruption typique avec Compose n’est pas la faute de Docker. C’est une migration qui prend un verrou exclusif, ou une réécriture de colonne, ou la création d’un index sans
concurrence. L’app cesse de répondre, le healthcheck échoue, Compose la redémarre, et maintenant vous avez un déni de service auto-infligé.
5) Services stateful cachés derrière des patterns « stateless »
Vous pouvez blue/green votre couche web. Vous ne pouvez pas blue/greeniser occasionnellement une base single-instance en « démarrant un autre conteneur » à moins que votre stockage,
réplication et basculement ne soient déjà mûrs. Compose peut exécuter Postgres. Compose n’est pas une solution HA Postgres.
6) Le tueur silencieux : pools de connexions et DNS obsolète
Si vous faites tourner les backends en remplaçant les IP de conteneur et que vous vous attendez à ce que les clients « se reconnectent juste », vous découvrirez que les pools de connexions
et le cache DNS ont leurs opinions. Certains drivers cachent les IP résolues plus longtemps que prévu. Certaines apps ne se reconnectent jamais sauf si vous les relancez. Voilà pourquoi
les noms de service stables (frontends proxy) sont importants.
Modèles opérationnels pour un quasi-zéro interruption sur Compose
Pattern A : Proxy inverse stable + services applicatifs versionnés (approche blue/green)
C’est le modèle que je recommande le plus souvent parce qu’il correspond aux forces de Compose : simple, local, déterministe. Vous gardez un conteneur proxy stable
lié aux ports hôtes (80/443). Votre application tourne derrière sur un réseau défini par l’utilisateur. Pendant le déploiement, vous démarrez un nouveau service applicatif (green)
à côté de l’ancien (blue), vérifiez la readiness, puis basculez l’upstream du proxy et retirez l’ancien de manière gracieuse.
Caractéristiques clés :
- Le proxy est la seule chose qui bind les ports de l’hôte.
- Les deux versions de l’app peuvent tourner simultanément sur le réseau interne.
- Le basculement du trafic est un rechargement de config, pas une recréation de conteneur.
- Le rollback consiste à remettre le proxy en arrière et tuer la version défaillante.
Vous pouvez implémenter le proxy avec Nginx, HAProxy, Traefik, Caddy. Choisissez celui que vous savez faire fonctionner. « Savoir opérer » signifie : vous pouvez le recharger en toute sécurité, lire ses logs,
et expliquer ce qui se passe quand un backend échoue.
Pattern B : Scale-out avec --scale + drain + recréation (rolling limité)
Compose supporte la montée en réplicas d’un service sur un seul hôte. Cela ne donne pas à lui seul des rolling updates, mais ça vous donne de la marge :
faites monter des réplicas supplémentaires de la nouvelle version, routez le trafic vers eux, puis supprimez les anciennes réplicas. La limitation est que Compose ne gère pas
nativement « l’ordre de mise à jour » comme un orchestrateur. Vous écrivez le playbook.
Cela marche mieux quand :
- Votre app est sans état ou l’état de session est externalisé.
- Votre proxy/load balancer peut détecter la santé du backend et arrêter d’y router.
- Vous acceptez une séquence manuelle ou un petit script de déploiement.
Pattern C : Activation de socket / propriété de port au niveau hôte (avancé, coins tranchants)
Si vous tenez à éviter un conteneur proxy, vous pouvez laisser systemd prendre le socket et le transmettre à l’instance d’app actuelle (socket activation).
Ça peut fonctionner. Ça peut aussi devenir un générateur d’incidents artisanal si votre app ne le supporte pas proprement ou si vous ne testez pas le rechargement
sous charge.
Pour la plupart des équipes qui utilisent Compose, le pattern proxy stable est le point d’équilibre. Moins d’astuce. Plus de fiabilité.
Pattern D : « Migrations ennuyeuses » + déploiement d’app (l’exigence cachée)
Même un échange de conteneurs parfait n’aide pas si votre migration lock la base pendant 30 secondes. Le pattern de déploiement doit inclure une discipline de base de données :
- Changements de schéma expand/contract (ajouter des colonnes/tables d’abord, déployer le code, puis supprimer l’ancien).
- Lectures/écritures compatibles ascendant pendant la transition.
- Construction d’index en ligne quand c’est possible.
- Feature flags quand un changement ne peut être instantané.
Compose ne vous empêche pas de faire ça. Compose ne vous le rappellera pas non plus. C’est à vous de vous le rappeler.
Blague #2 : Une migration avec verrou exclusif est la seule chose qui peut faire tomber votre app plus vite que votre CEO essayant de « donner un coup de main au déploiement ».
Tâches pratiques : commandes, sorties et décisions (12+)
Voici les vérifications que je lance réellement sur un hôte Compose quand j’essaie de rendre le « zéro interruption » concret. Chaque tâche inclut : la commande, à quoi ressemble une sortie typique,
ce que ça signifie, et quelle décision prendre ensuite.
Task 1: Confirm what Compose thinks is running
cr0x@server:~$ docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
prod-proxy-1 nginx:1.25-alpine "/docker-entrypoint.…" proxy 2 weeks ago Up 2 weeks 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
prod-app-blue-1 registry/app:1.9.3 "/app/start" app-blue 2 weeks ago Up 2 weeks (healthy)
prod-app-green-1 registry/app:1.9.4 "/app/start" app-green 3 minutes ago Up 3 minutes (healthy)
Ce que ça signifie : Vous avez deux versions de l’app en ligne et un proxy stable.
Décision : Si green est healthy, vous êtes prêt à basculer le trafic en changeant l’upstream du proxy, pas en arrêtant blue.
Task 2: Inspect healthcheck status and timing
cr0x@server:~$ docker inspect --format '{{json .State.Health}}' prod-app-green-1
{"Status":"healthy","FailingStreak":0,"Log":[{"Start":"2026-01-03T10:12:01.123Z","End":"2026-01-03T10:12:01.187Z","ExitCode":0,"Output":"ok\n"}]}
Ce que ça signifie : Le conteneur rapporte une readiness (à condition que votre healthcheck soit pertinent).
Décision : Si la santé est starting ou unhealthy, ne basculez pas le trafic. Corrigez la readiness ou le démarrage de l’app d’abord.
Task 3: Verify the proxy sees both backends and which one is active
cr0x@server:~$ docker exec -it prod-proxy-1 nginx -T | sed -n '1,120p'
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
# configuration content:
upstream app_upstream {
server prod-app-blue-1:8080 max_fails=2 fail_timeout=5s;
server prod-app-green-1:8080 max_fails=2 fail_timeout=5s;
}
server {
listen 80;
location / {
proxy_pass http://app_upstream;
}
}
Ce que ça signifie : Le proxy est configuré pour faire du load balancing entre les deux.
Décision : Si vous voulez un basculement strict (et pas de load balancing), configurez des poids ou passez à une cible upstream unique pour la coupure.
Task 4: Validate Nginx reload is graceful (no dropped workers)
cr0x@server:~$ docker exec -it prod-proxy-1 nginx -s reload && docker exec -it prod-proxy-1 tail -n 5 /var/log/nginx/error.log
2026/01/03 10:15:22 [notice] 1#1: signal process started
2026/01/03 10:15:22 [notice] 1#1: reconfiguring
2026/01/03 10:15:22 [notice] 1#1: using the "epoll" event method
Ce que ça signifie : Le reload a réussi ; Nginx ne s’est pas relancé depuis zéro.
Décision : Si le reload renvoie une erreur, ne déployez pas. Corrigez la génération de config et exécutez nginx -t avant le reload.
Task 5: Confirm stop timeout and signal behavior (graceful shutdown)
cr0x@server:~$ docker inspect --format 'StopSignal={{.Config.StopSignal}} StopTimeout={{.Config.StopTimeout}}' prod-app-blue-1
StopSignal=SIGTERM StopTimeout=30
Ce que ça signifie : Docker enverra SIGTERM puis attendra 30s avant SIGKILL.
Décision : Si votre app a besoin de 60s pour drainer, définissez stop_grace_period: 60s. Si elle ignore SIGTERM, corrigez l’app. Aucun YAML ne vous sauvera.
Task 6: Watch in-flight connections during a drain window
cr0x@server:~$ docker exec -it prod-app-blue-1 ss -Hnt state established '( sport = :8080 )' | wc -l
47
Ce que ça signifie : 47 connexions TCP établies vers l’app.
Décision : Si le nombre ne baisse pas après avoir retiré le backend du proxy, vous avez peut-être des connexions longues (websockets) et vous aurez besoin d’un plan de drain plus long.
Task 7: Confirm which container is actually receiving traffic
cr0x@server:~$ docker logs --since=2m prod-app-green-1 | tail -n 5
10.0.2.5 - - [03/Jan/2026:10:16:01 +0000] "GET /healthz HTTP/1.1" 200 2 "-" "kube-probe/1.0"
10.0.2.5 - - [03/Jan/2026:10:16:04 +0000] "GET /api/orders HTTP/1.1" 200 431 "-" "Mozilla/5.0"
Ce que ça signifie : Green reçoit de vraies requêtes.
Décision : Si seul les health checks frappent green, vous n’avez pas encore basculé le trafic de production ; changez le routage ou les poids du proxy délibérément.
Task 8: Detect whether you’re accidentally recreating the proxy (the outage generator)
cr0x@server:~$ docker compose up -d --no-deps proxy
[+] Running 1/0
✔ Container prod-proxy-1 Running
Ce que ça signifie : Compose n’a pas recréé le conteneur proxy.
Décision : Si ceci affiche « Recreated », vous faites flapper la porte d’entrée. Arrêtez. Pincez les changements de configuration du proxy et recharger intra-conteneur à la place.
Task 9: Compare running image digests (avoid “latest” surprises)
cr0x@server:~$ docker images --digests registry/app | head -n 5
REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
registry/app 1.9.4 sha256:8b9a2f6d3c1e8f... 4a1f2c3d4e5f 2 days ago 156MB
registry/app 1.9.3 sha256:1c2d3e4f5a6b7c... 7b6a5c4d3e2f 2 weeks ago 155MB
Ce que ça signifie : Vous pouvez identifier exactement ce qui est déployé.
Décision : Si vous utilisez :latest sans digests, arrêtez cela. Étiquetez vos releases et gardez une image de rollback connue.
Task 10: Check Docker events during deploy (find who restarted what)
cr0x@server:~$ docker events --since 10m --until 0m | tail -n 12
2026-01-03T10:12:10.000000000Z container start 2f1a... (name=prod-app-green-1, image=registry/app:1.9.4)
2026-01-03T10:14:02.000000000Z container health_status: healthy 2f1a... (name=prod-app-green-1)
2026-01-03T10:15:22.000000000Z container exec_start: nginx -s reload 9aa2... (name=prod-proxy-1)
Ce que ça signifie : Chronologie de ce qui s’est réellement passé, pas de ce que vous vous rappelez avoir fait.
Décision : Si vous voyez des événements de restart/recreate du proxy pendant le déploiement, votre histoire de « zéro interruption » a une faille.
Task 11: Verify kernel-level resource pressure (CPU throttling and IO wait)
cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 51200 21000 320000 0 0 120 80 600 1200 25 10 55 10 0
3 1 0 48000 20500 318000 0 0 2200 300 900 1700 30 12 38 20 0
Ce que ça signifie : Le deuxième échantillon montre une IO wait plus élevée (wa) et des processus bloqués (b).
Décision : Si l’IO wait monte pendant le déploiement (pull d’image, migrations), planifiez le déploiement hors-peak, placez les images dans un registre/cache local, ou corrigez les performances de stockage.
Task 12: Identify which process owns the host port (catch accidental binds)
cr0x@server:~$ sudo ss -Htlpn '( sport = :443 )'
LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("docker-proxy",pid=2143,fd=4))
Ce que ça signifie : Le docker proxy possède le port publié. Si le conteneur est recréé, le bind va flapper.
Décision : Gardez un conteneur proxy stable. Ne publiez pas les ports d’app directement si vous voulez des swaps transparents.
Task 13: Confirm network attachments (is the proxy on the right network?)
cr0x@server:~$ docker inspect --format '{{range .NetworkSettings.Networks}}{{.NetworkID}} {{end}}' prod-proxy-1
a8c1f0d3e2b1c4d5e6f7a8b9c0d1e2f3
Ce que ça signifie : Le proxy est sur au moins un réseau défini par l’utilisateur.
Décision : Si le proxy et l’app ne sont pas sur le même réseau, la résolution de noms comme prod-app-green-1 ne fonctionnera pas. Réparez les réseaux avant d’accuser Docker.
Task 14: Check database locks during migration (the downtime smoking gun)
cr0x@server:~$ docker exec -it prod-db-1 psql -U postgres -d app -c "select pid, wait_event_type, wait_event, state, query from pg_stat_activity where state <> 'idle' order by pid;"
pid | wait_event_type | wait_event | state | query
------+-----------------+---------------+--------+----------------------------------------
2412 | Lock | relation | active | ALTER TABLE orders ADD COLUMN foo text;
2550 | Lock | transactionid | active | UPDATE orders SET foo = 'x' WHERE ...
Ce que ça signifie : Sessions actives en attente de verrous. Votre « outage de déploiement » peut être un verrou de schéma.
Décision : Arrêtez la migration si elle est dangereuse, ou redesign-la (approche en ligne, batching, indexes concurrents). Ne continuez pas à redémarrer les conteneurs applicatifs en espérant que ça résolve le problème.
Task 15: Confirm Compose didn’t silently change containers due to config drift
cr0x@server:~$ docker compose config | sed -n '1,120p'
name: prod
services:
app-blue:
image: registry/app:1.9.3
healthcheck:
test:
- CMD
- /bin/sh
- -c
- curl -fsS http://localhost:8080/ready || exit 1
interval: 5s
timeout: 2s
retries: 12
stop_grace_period: 60s
proxy:
image: nginx:1.25-alpine
ports:
- mode: ingress
target: 80
published: "80"
protocol: tcp
- mode: ingress
target: 443
published: "443"
protocol: tcp
Ce que ça signifie : Ceci est la configuration entièrement rendue que Compose applique.
Décision : Si la sortie de config diffère de ce que vous pensez avoir déployé, corrigez votre gestion des variables d’environnement et figez les valeurs. « Surprise config » est l’ami de l’indisponibilité.
Trois mini-récits d’entreprise depuis le terrain
Mini-récit 1 : L’incident causé par une mauvaise hypothèse (« depends_on signifie ready »)
Une équipe SaaS de taille moyenne faisait tourner sa stack de production sur une seule VM avec Docker Compose. C’était un choix délibéré : elles voulaient moins de pièces mobiles,
et leur trafic tenait confortablement sur un hôte. Sensé. Le système avait une API, un worker, Redis et Postgres.
Ils ont fait une mise à jour routinière : nouvelle image API, migration mineure, redémarrage. Le playbook de déploiement était une seule ligne : docker compose up -d.
Ils ont supposé que leurs réglages depends_on signifiaient que l’API attendrait Postgres. Ce n’était pas le cas.
Postgres a mis plus de temps que d’habitude à démarrer parce que la VM tirait aussi des images et faisait des écritures sur le système de fichiers. L’API a démarré, n’a pas pu se connecter,
est sortie, a redémarré, a échoué de nouveau. Leur politique de redémarrage a transformé un démarrage lent en boucle serrée. Pendant ce temps le proxy inverse routait vers un backend instable.
Les utilisateurs ont vu des 502 intermittents pendant plusieurs minutes — assez longtemps pour des tickets support et une partie de blâme interne.
La correction n’était pas exotique. Ils ont ajouté un vrai endpoint de readiness à l’API, lié un healthcheck dessus, et configuré le proxy pour ne router qu’aux backends sains.
Ils ont aussi ajouté une période de démarrage pour que des échecs transitoires n’alertent pas instantanément la santé.
La leçon était inconfortable parce que banale : Compose ordonne les démarrages ; il ne garantit pas la readiness des dépendances. Ils ont arrêté de supposer que « démarré » signifiait « prêt », et leurs déploiements ont cessé d’être dramatiques.
Mini-récit 2 : L’optimisation qui s’est retournée contre eux (déploiements rapides, disques lents)
Une autre organisation faisait tourner Compose sur un hôte costaud avec NVMe — jusqu’à ce que l’approvisionnement le remplace par du matériel « similaire » avec beaucoup de CPU et RAM mais un stockage médiocre.
L’équipe a optimisé les déploiements en tirant toujours des images fraîches juste avant la mise à jour. Plus de rapidité, moins de mismatch « marche en CI ». Sur le papier.
Sous charge, le déploiement a téléchargé un grand jeu de couches d’images, saturant l’IO. Le checkpointing de Postgres a ralenti. Les latences API ont augmenté. Les healthchecks ont commencé à timeouter.
Le proxy a marqué les backends comme unhealthy. Soudain « déployer plus vite » est devenu « le déploiement provoque un brownout en cascade ».
Ils ont répondu en resserrant les seuils de healthcheck pour « être plus sensibles », ce qui était exactement la mauvaise intuition. Cela a fait éjecter les backends plus tôt, amplifiant la panne.
Leur burn rate SLO a grimpé, et tout le monde a appris la différence entre « détecter une défaillance » et « provoquer une défaillance ».
La correction finale : pré-puller les images pendant les périodes calmes, limiter l’impact IO (en utilisant l’ordonnancement au niveau hôte et parfois simplement de la discipline humaine), et rendre les healthchecks tolérants aux pics courts.
Ils ont aussi déplacé le répertoire Docker vers un stockage plus rapide et séparé l’IO de la base de données de l’extraction d’images autant que possible.
La leçon d’optimisation : la vitesse n’est pas gratuite. Si vous rendez les déploiements plus rapides en déplaçant du travail dans la fenêtre critique, vos utilisateurs en paieront le prix.
Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la situation (deux versions + rollback facile)
Une équipe d’entreprise régulée — beaucoup de processus, beaucoup de paperasse — faisait tourner un portail client sur Compose. Ils n’étaient pas tendance. Ils étaient aussi rarement en panne.
Leur méthode de déploiement ressemblait à de la vieille école : proxy stable, deux services applicatifs (« blue » et « green »), gates de santé explicites, et un switch manuel du trafic.
Un vendredi, la nouvelle release a passé les tests mais contenait une fuite mémoire subtile déclenchée par un patron de requête rare. Vingt minutes après la coupure, le RSS a commencé à monter doucement.
La latence a augmenté. Le responsable on-call a regardé se dérouler la situation avec le calme de quelqu’un qui a répété ça.
Ils ont remis le trafic sur blue en reloadant la config du proxy, puis arrêté green. Le rollback a pris moins de temps que d’expliquer pourquoi ils rollbackaient. Les clients ont à peine remarqué : un petit pic de latence, pas de bannière d’indisponibilité, pas de panique.
Plus tard, dans le postmortem, personne n’a loué le YAML. Ils ont loué la discipline ennuyeuse : garder toujours la version précédente en fonctionnement jusqu’à ce que la nouvelle fasse ses preuves, et rendre le rollback une action réversible en un seul geste, pas une danse héroïque en plusieurs étapes.
La leçon : la fiabilité est surtout une répétition peu glamour. Si votre déploiement exige du courage, vous avez déjà perdu.
Procédure de diagnostic rapide : trouver le goulot vite
Quand un « déploiement zéro interruption » provoque un accroc, vous n’avez pas le temps pour des débats philosophiques. Vous avez besoin d’une séquence de triage rapide qui réduit le coupable à : proxy/basculement de trafic, readiness app, base de données ou ressources hôte.
Premier : la porte d’entrée est-elle stable ?
- Vérifiez si le conteneur proxy a été recréé ou redémarré pendant le déploiement (
docker events, uptime viadocker ps). - Vérifiez la continuité du bind des ports hôtes (
ss -ltnppour 80/443). - Vérifiez les logs du proxy pour des échecs d’upstream et des erreurs de reload.
Si le proxy a flappé, c’est votre interruption. Corrigez le process pour que le proxy reste actif et ne fasse que recharger la config.
Second : les backends sont-ils sains et réellement prêts ?
- Inspectez le statut de santé des nouveaux conteneurs.
- Appelez le endpoint de readiness depuis le réseau du proxy.
- Confirmez que le proxy route vers l’ensemble de backends prévu.
Si les healthchecks disent « healthy » mais que l’app échoue sous charge, votre healthcheck ment. Rendez-le plus représentatif.
Troisième : la base de données bloque-t-elle le monde ?
- Vérifiez les requêtes actives et les verrous pendant les fenêtres de migration.
- Cherchez des erreurs/timeout de connexion dans les logs applicatifs.
- Vérifiez l’IO wait et le comportement de checkpoint de la DB (les métriques hôte aident).
Si les verrous s’accumulent, arrêtez de redéployer. Réparez les migrations. Les verrous de schéma n’obéissent pas à l’optimisme.
Quatrième : l’hôte est-il sous pression de ressources ?
- Vols CPU/throttle, IO wait, pression mémoire.
- Pulls d’images Docker et extraction de couches pendant le déploiement.
- Saturation disque sur le répertoire Docker et le volume de la DB.
Si l’hôte étouffe, le meilleur pattern de déploiement au monde ne vous sauvera pas. La fiabilité commence par une capacité ennuyeuse.
Erreurs communes : symptôme → cause racine → correction
1) Symptom: brief outage on every deploy (a few seconds of 502)
Cause racine : Le conteneur app possède le port de l’hôte ; le redémarrage/recréation le fait flapper.
Correction : Placez un proxy stable sur les ports hôtes ; routez vers l’app sur un réseau interne. Déployez en ajoutant d’abord un nouveau backend.
2) Symptom: new version starts, immediately gets traffic, returns 500 for 10–30 seconds
Cause racine : Pas de gating de readiness ; le healthcheck teste seulement « le process existe » ou est absent.
Correction : Implémentez un vrai endpoint /ready qui vérifie les dépendances ; utilisez Docker healthcheck et gatez le proxy selon celui-ci.
3) Symptom: deploy triggers a restart loop; logs show DB connection failures
Cause racine : DB pas prête, migrations en cours, ou contention de lock ; la politique de restart amplifie le problème.
Correction : Ajoutez des start periods ; évitez les loops de restart pendant les migrations ; séparez le job de migration du démarrage de l’app ; rendez les migrations en ligne et incrémentales.
4) Symptom: connections drop during deploy even though proxy stays up
Cause racine : L’app ne gère pas SIGTERM ; stop timeout trop court ; connexions longues non drainées.
Correction : Implémentez un arrêt gracieux ; augmentez stop_grace_period ; configurez le proxy pour arrêter de router avant d’arrêter les conteneurs.
5) Symptom: rollback takes longer than deploy and is risky
Cause racine : Pas d’exécution parallèle ; le déploiement remplace in-place ; changements DB non compatibles en arrière.
Correction : Services backend blue/green ; gardez la version précédente exécutable et routable ; utilisez des migrations expand/contract et des feature flags.
6) Symptom: “It worked in staging” but production deploy causes latency spikes
Cause racine : Contention de ressources en production (IO, CPU) pendant les pulls d’images/migrations ; healthchecks trop agressifs.
Correction : Pré-pull des images ; planifiez les opérations lourdes ; ajustez les timeouts/retries des healthchecks ; séparez les chemins IO pour la DB et Docker quand possible.
7) Symptom: proxy routes to dead backends after deploy
Cause racine : Config upstream du proxy utilise des IP statiques ou des noms de conteneur stale ; mismatch d’attachement réseau.
Correction : Utilisez la découverte de service via Docker DNS sur un réseau défini par l’utilisateur ; référencez des noms de service ; assurez-vous que le proxy est sur le même réseau.
8) Symptom: Compose “up -d” recrée plus que prévu
Cause racine : Dérive de config (changement d’env, volumes, tags d’image) déclenche des recréations ; le proxy n’est pas figé.
Correction : Verrouillez les env ; utilisez docker compose config pour inspecter la config finale ; évitez de changer le conteneur proxy sauf si nécessaire ; rechargez la config à la place.
Checklists / plan étape par étape
Checklist 1: Minimum viable « quasi-zéro interruption » pour une stack Compose
- Conteneur proxy stable publie les ports hôtes 80/443.
- Backends applicatifs non publiés sur les ports hôtes ; seulement exposés sur le réseau interne.
- Healthchecks de readiness (pas « le process existe »).
- Arrêt gracieux : gestion de SIGTERM + stop grace period suffisant.
- Migrations BD séparées du démarrage de l’app et conçues pour des changements en ligne.
- Plan de rollback : garder la version précédente exécutable et routable tant que la nouvelle ne s’est pas prouvée.
Checklist 2: Déploiement étape par étape (blue/green avec switch proxy)
-
Pré-pull de l’image pour éviter les pics IO pendant la fenêtre critique.
cr0x@server:~$ docker pull registry/app:1.9.4 1.9.4: Pulling from app Digest: sha256:8b9a2f6d3c1e8f... Status: Downloaded newer image for registry/app:1.9.4Décision : Si le pull prend trop de temps ou spike l’IO, faites-le plus tôt ou corrigez le stockage.
-
Démarrez le nouveau backend à côté de l’ancien.
cr0x@server:~$ docker compose up -d app-green [+] Running 1/1 ✔ Container prod-app-green-1 StartedDécision : Si cela recrée blue ou le proxy, votre modèle Compose est erroné. Arrêtez et isolez les services.
-
Attendez la readiness de green.
cr0x@server:~$ docker inspect --format '{{.State.Health.Status}}' prod-app-green-1 healthyDécision : Si unhealthy, rollback en arrêtant green et examinez les logs.
-
Changez le routage du proxy (poids ou cible upstream unique), puis reload.
cr0x@server:~$ docker exec -it prod-proxy-1 nginx -t nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successfulcr0x@server:~$ docker exec -it prod-proxy-1 nginx -s reload 2026/01/03 10:20:12 [notice] 1#1: signal process startedDécision : Si
nginx -téchoue, ne reload pas. Corrigez la génération de config d’abord. -
Observez le trafic et les erreurs pendant une période de soak.
cr0x@server:~$ docker exec -it prod-proxy-1 tail -n 10 /var/log/nginx/access.log 10.0.1.10 - - [03/Jan/2026:10:20:15 +0000] "GET /api/orders HTTP/1.1" 200 431 "-" "Mozilla/5.0" 10.0.1.11 - - [03/Jan/2026:10:20:16 +0000] "POST /api/pay HTTP/1.1" 200 1024 "-" "Mozilla/5.0"Décision : Si vous voyez des 502/504 en upstream, vérifiez la readiness des backends, les verrous BD, et les timeouts du proxy.
-
Drainer et arrêter blue après prise de confiance.
cr0x@server:~$ docker compose stop app-blue [+] Running 1/1 ✔ Container prod-app-blue-1 StoppedDécision : Si vous avez encore besoin d’un rollback instantané, ne supprimez pas blue tout de suite — gardez-le arrêté mais disponible, ou gardez-le en cours d’exécution mais non routé.
Checklist 3: Discipline des changements de base de données pour les déploiements Compose
- Ne combinez jamais des migrations risquées avec un déploiement que vous ne pouvez pas rollbacker.
- Privilégiez les changements additifs d’abord (nouvelle colonne nullable, nouvelle table, nouvel index concurrent si supporté).
- Backfill par lots via un job/worker avec limitation de débit.
- Basculez lectures/écritures vers le nouveau schéma via un feature flag ou un chemin de code versionné.
- Ce n’est qu’ensuite que vous supprimez les anciennes colonnes/tables dans un déploiement ultérieur.
FAQ
1) Docker Compose peut-il faire de vrais déploiements zéro interruption ?
Pas en tant que fonctionnalité d’orchestration intégrée. Vous pouvez obtenir un zéro visible pour l’utilisateur pour beaucoup d’apps web en gardant un proxy stable et
en échangeant des backends sains derrière lui, plus un arrêt gracieux et des migrations sensées.
2) Pourquoi ne pas simplement utiliser docker compose up -d et faire confiance ?
Parce que Compose réconcilie l’état désiré en recréant des conteneurs quand il détecte des changements. Si le conteneur possède le port hôte, la recréation équivaut à un flap de port.
Ce n’est pas de la malveillance ; c’est du design.
3) depends_on garantit-il que mon app attend la base de données ?
Non. Il impose l’ordre de démarrage, pas la readiness. Utilisez des healthchecks, une logique d’attente explicite dans votre app, ou un processus de déploiement qui vérifie la readiness des dépendances.
4) Quel est le modèle le plus simple et viable ?
Proxy inverse stable sur les ports hôtes + deux services applicatifs (blue/green) sur un réseau défini par l’utilisateur. Démarrez green, vérifiez la santé, reload du proxy pour router vers
green, puis drainer et arrêter blue.
5) Dois-je utiliser Traefik pour ça ?
Traefik convient si vous savez déjà l’opérer. Il brille pour la configuration dynamique via des labels de conteneur. Mais « dynamique » ne veut pas dire « sûr » ; vous avez toujours besoin
de healthchecks, d’un comportement de drainage et d’un plan de rollback.
6) Et les websockets et connexions longues ?
Prévoyez le drainage des connexions. Beaucoup de clients websocket se déconnectent au redémarrage du backend. Vous pouvez réduire la douleur en augmentant les périodes de grâce, en arrêtant
le routage avant l’arrêt, et en concevant des clients capables de se reconnecter proprement. « Zéro interruption » peut encore signifier « quelques reconnexions ».
7) Puis-je faire des rolling updates avec --scale ?
Vous pouvez faire un playbook manuel de rolling update : scaler la nouvelle version, router le trafic, puis scaler l’ancienne vers le bas. Compose ne le coordonnera pas pour vous, donc
vous devez écrire et tester les étapes.
8) Quelle est la cause cachée la plus fréquente d’indisponibilité pendant les déploiements Compose ?
Les migrations de base de données qui lockent les tables ou saturent l’IO. L’échange de conteneurs est généralement facile. Le changement de schéma est le vrai combat.
9) Est-il plus sûr d’exécuter les migrations au démarrage du conteneur ?
Généralement non. Cela couple le succès du déploiement au succès de la migration, encourage le « restart jusqu’à ce que ça marche », et peut provoquer des herds si plusieurs réplicas démarrent.
Préférez une étape de migration contrôlée et explicite.
10) Quand devrais-je arrêter d’utiliser Compose en production ?
Quand vous avez besoin d’un scheduling multi-hôte, d’un self-healing entre machines, de rolling updates automatiques avec gestion du trafic, ou d’une distribution robuste des secrets/config.
À ce stade, vous voulez un orchestrateur, pas un script bash très discipliné.
Conclusion : prochaines étapes pratiques
Compose ne vous donne pas le zéro interruption. Il vous donne une définition propre et lisible de ce qui doit tourner sur un hôte. Le reste — basculement de trafic, readiness,
drainage et discipline des schémas — dépend de vous. Ce n’est pas une plainte. C’est le contrat.
Si vous voulez des mises à jour quasi sans interruption sur Compose, faites ceci ensuite :
- Placez un proxy inverse stable devant tout et arrêtez de publier les ports applicatifs directement.
- Ajoutez un vrai endpoint de readiness et branchez-le aux healthchecks Docker et au routage du proxy.
- Implémentez un arrêt gracieux : gestion SIGTERM, stop grace period adéquat et drain-before-stop.
- Séparez les migrations du démarrage de l’app et adoptez les changements de schéma expand/contract.
- Rédigez un playbook de déploiement que vous pouvez exécuter à 3h du matin — et répétez le rollback jusqu’à ce que ce soit ennuyeux.
Ensuite testez sous charge. Pas en théorie. Pas en staging avec trois requêtes par minute. Dans quelque chose qui ressemble à la production, où le système est déjà occupé à faire son travail pendant que vous essayez d’en remplacer des parties.