14h23 : la prod vient de tomber
La pipeline GitHub Actions vient de passer au vert. Tu souffles. Une minute plus tard, ton téléphone vibre : le site est en 500. Pourtant, l'image Docker a bien été construite et poussée sur GHCR. Le souci, c'est ce qui se passe sur le VPS après : le docker compose pull démarre, mais la migration de base de données plantée à mi-chemin laisse le schéma incohérent. L'app tourne, mais sur une base à moitié migrée. Ce n'est pas un bug de l'app : c'est une faiblesse du processus.
Ce que tu viens de vivre illustre ce qui peut encore casser avec Docker, même si une image est tout-ou-rien. Une image est un artefact complet ou absent : pas d'état à moitié construit, pas de modules manquants. Et docker compose up -d --wait ne déclare la bascule réussie que si le conteneur passe à l'état healthy. Mais deux pièges restent : un bug logique qui passe les tests, et une migration qui se passe mal. D'où la règle : savoir revenir en arrière en quelques secondes.
Docker règle le problème du déploiement partiel (l'image est tout-ou-rien), mais pas celui du bug logique ni celui de la migration ratée. Si ton healthcheck renvoie 500 après le up --wait, tu dois pouvoir basculer sur la version d'avant sans délai ni compilation.
Le double tag : main et sha
La CI (leçon 6) construit l'image et la pousse sur GHCR avec deux tags à chaque commit :
:main: la dernière version connue comme bonne, que le VPS suit en temps normal.:sha-e4f5g6h: la photo immuable de ce commit précis. Elle ne bouge jamais.
Sur le VPS, le compose.yml déclare l'image avec une variable :
services:
php:
image: ghcr.io/toi/ton-projet:${TAG:-main}
Et le .env à côté contient simplement :
TAG=main
En temps normal, le VPS tire toujours l'image :main, la plus récente. La photo :sha- est là pour le rollback.
Le tag :sha- reste sur le disque du VPS après le premier pull. Si tu dois revenir dessus, la bascule ne nécessite souvent aucun téléchargement : Docker réutilise les layers déjà en cache.
Le rollback : changer un tag, 15 secondes
Le healthcheck renvoie 500 après le deploy de i7j8k9l. Tu n'as pas à recompiler, pas à cloner le dépôt, pas à relancer la CI. Tu échanges une photo contre une autre :
# 1. Quelle était la version saine d'hier ?
# (l'historique est sur GHCR et dans git log)
git log --oneline -3
# e4f5g6h hier 18h30 - feat: page contact
# i7j8k9l il y a 2 min - feat: refacto email (CASSÉ)
# 2. Sur le VPS : pointer le tag sain dans le .env
TAG=sha-e4f5g6h
# 3. Appliquer et attendre le feu vert du healthcheck
docker compose up -d --wait
Ce que fait up -d --wait : il recrée uniquement les conteneurs dont l'image a changé, attend que chacun passe healthy, et sort avec un code 0 si tout va bien ou 1 si le healthcheck échoue. La bascule prend 10 à 15 secondes. Aucune compilation. Aucun git. Tu échanges une photo contre une autre.
Teste le rollback une fois par mois, comme un exercice incendie. Le jour où tu en as besoin pour de vrai, tu n'as pas envie de le découvrir sous pression.
Les migrations : re-pointer l'image ne dé-migre pas la base. Si le commit cassé a ajouté une colonne en base, cette colonne est toujours là après le rollback d'image. Règle d'or : écris des migrations rétrocompatibles. Ajouter une colonne, oui. La supprimer dans le même commit que le code qui l'utilisait encore, non. Le rollback d'image ne règle pas un schéma cassé : planifie ta migration en conséquence.
La méthode classique, sans Docker
Sans Docker, le même principe s'appelle le pattern Capistrano/Deployer : on construit la nouvelle version dans un dossier daté à côté (releases/20260603_1423/), et on bascule d'un coup en re-pointant un symlink current vers ce dossier. Rollback = re-pointer current vers le dossier précédent + systemctl restart mon-service. Même logique (préparer à côté, basculer d'un coup, garder l'ancien), implémentée avec des dossiers et un symlink au lieu d'images et de tags.
/opt/app/
├── releases/
│ ├── 20260601_0910/ ← avant-hier
│ ├── 20260602_1830/ ← hier soir (saine)
│ └── 20260603_1423/ ← cassée
└── current -> releases/20260602_1830
Le schéma : images taguées sur GHCR, tag du .env
TAG dans le .env vers un sha immuable et relancer docker compose up -d --wait. Aucune compilation. Les volumes (base de données) ne bougent pas.Pendant que docker compose up -d --wait recrée le conteneur php sur l'image d'hier, que voit le visiteur, et que deviennent les données de la base ?
Voir la réponse
Coupure quasi nulle le temps de la recréation du conteneur (quelques secondes), puis le site revient. Les données de la base ne bougent pas : elles vivent dans le volume pgdata, indépendant des images. Changer de tag ne touche pas aux volumes.
À toi : rollback par tag en situation réelle
Le deploy de i7j8k9l casse la prod à 14h23. Le healthcheck renvoie 500. Tu as les images sur GHCR et en cache local. Mission : revenir sur sha-e4f5g6h sans rien compiler.
14:23 — production just went down
The GitHub Actions pipeline just went green. You breathe. A minute later, your phone buzzes: the site is returning 500. The Docker image was built and pushed to GHCR just fine. The issue happened on the VPS: the docker compose pull started, but a database migration crashed halfway, leaving an inconsistent schema. The app is running — on a half-migrated database. This isn't a bug in the app: it's a weakness in the process.
What you just witnessed shows what can still go wrong with Docker, even though a Docker image is all-or-nothing. An image is a complete or absent artifact: no half-built state, no missing modules. And docker compose up -d --wait only declares the switch successful once the container reaches healthy. But two traps remain: a logic bug that slips through tests, and a migration that goes wrong. Hence the rule: know how to roll back in a matter of seconds.
Docker solves the partial-deploy problem (an image is all-or-nothing), but not logic bugs or failed migrations. If your healthcheck returns 500 after the up --wait, you must be able to switch back to the previous version without delay or compilation.
The double tag: main and sha
The CI (lesson 6) builds the image and pushes it to GHCR with two tags on every commit:
:main— the latest version known to be good, which the VPS follows in normal operation.:sha-e4f5g6h— the immutable snapshot of that exact commit. It never changes.
On the VPS, compose.yml declares the image with a variable:
services:
php:
image: ghcr.io/you/your-project:${TAG:-main}
And the .env next to it contains simply:
TAG=main
Under normal operation the VPS always pulls the latest :main image. The :sha- snapshot is there for rollback.
The :sha- tag stays on the VPS disk after the first pull. If you need to roll back to it, the switch often requires no download at all: Docker reuses the layers already in cache.
The rollback: change a tag, 15 seconds
The healthcheck returns 500 after the i7j8k9l deploy. No recompilation, no repo clone, no CI re-run. You swap one snapshot for another:
# 1. What was the healthy version from yesterday?
# (history is on GHCR and in git log)
git log --oneline -3
# e4f5g6h yesterday 18:30 - feat: contact page
# i7j8k9l 2 min ago - feat: email refacto (BROKEN)
# 2. On the VPS: point to the healthy tag in .env
TAG=sha-e4f5g6h
# 3. Apply and wait for the healthcheck green light
docker compose up -d --wait
What up -d --wait does: it recreates only the containers whose image changed, waits for each to become healthy, and exits with code 0 on success or 1 if the healthcheck fails. The switch takes 10 to 15 seconds. No compilation. No git. You swap one snapshot for another.
Test the rollback once a month, like a fire drill. The day you need it for real, you don't want to discover it under pressure.
Watch out for migrations: re-pointing the image does not un-migrate the database. If the broken commit added a column, that column is still there after the image rollback. Golden rule: write backward-compatible migrations. Adding a column, yes. Dropping it in the same commit as the code that was still using it, no. An image rollback can't fix a broken schema — plan your migrations accordingly.
The classic approach, without Docker
Without Docker, the same principle is called the Capistrano/Deployer pattern: you build the new version in a dated folder alongside the live one (releases/20260603_1423/), then switch atomically by re-pointing a current symlink. Rollback = re-point current at the previous folder + systemctl restart my-service. Same logic (prepare alongside, switch atomically, keep the old one), implemented with folders and a symlink instead of images and tags.
/opt/app/
├── releases/
│ ├── 20260601_0910/ ← day before yesterday
│ ├── 20260602_1830/ ← last night (healthy)
│ └── 20260603_1423/ ← broken
└── current -> releases/20260602_1830
The diagram: tagged images on GHCR, .env tag
TAG in the .env to an immutable sha and re-running docker compose up -d --wait. No compilation. Volumes (database) are untouched.While docker compose up -d --wait recreates the php container on yesterday's image, what does the visitor see, and what happens to the database data?
Show the answer
Near-zero downtime during the container recreation (a few seconds), then the site comes back. The database data doesn't move: it lives in the pgdata volume, independent of the images. Changing the tag doesn't touch volumes.
Your turn: tag-based rollback in a real situation
The i7j8k9l deploy broke production at 14:23. The healthcheck returns 500. You have the images on GHCR and in local cache. Mission: get back to sha-e4f5g6h without compiling anything.
🎯 Pratique
S'entraîner (clique pour ouvrir) :
💬 Ré-explique sans regarder
Explique le double tag :main / :sha- et comment le rollback fonctionne sur le VPS.
:main flottant + :sha- immuable) ; le VPS lit le TAG du .env ; rollback = changer TAG vers un sha sain + docker compose up -d --wait ; les volumes (base de données) ne bougent pas ; les migrations ne sont pas annulées.🧠 Rappel libre
La prod casse après un deploy Docker : cite les étapes du retour arrière.
git log --oneline). 2) Changer TAG=sha-SAIN dans le .env. 3) docker compose up -d --wait. Les volumes (base de données) ne bougent pas. Total : ~15 secondes.⚖️ Juge le conseil de l'IA
La prod est cassée. L'IA te dit : "Fais docker compose down puis docker compose up -d avec l'ancienne image, ça repart proprement." Tu acceptes ou tu rejettes ?
docker compose down coupe TOUT : les services applicatifs ET les volumes nommés si mal configurés, ce qui peut effacer des données. La bonne commande est docker compose up -d --wait : elle recrée uniquement les conteneurs dont l'image a changé, laisse les volumes intacts, et attend que le healthcheck soit vert. Jamais de down pour un simple rollback.The rollback saves you from a logic bug — but not from a failing disk or a burned server. In lesson 9, we set up the bare minimum that truly saves you: automated off-site backups every night, and an uptime alert that wakes you before your users notice. An image rollback won't save your data — a dump will.
Lesson 9: Backups and monitoring →