L'image qui tourne en production doit être exactement celle que vous avez testée en recette. Si chaque environnement la reconstruit, vous testez une image et vous en déployez une autre, même avec un Dockerfile identique. Cette leçon vous montre comment construire une seule fois, transférer l'archive sur le second hôte, déployer deux environnements qui ne diffèrent que par leur configuration, puis prouver que l'image en service est bien celle que vous avez préparée. Les sorties viennent de Docker Engine 29.9.2 et de Compose v5.6.1 sur Debian 13, avec le magasin d'images containerd, sur deux hôtes.
Trois mots reviennent partout. Un artefact est le résultat d'une construction que l'on déplace sans le refaire : ici, une image Docker. Son Id est l'empreinte sha256 qui l'identifie, et deux images de même Id sont identiques octet pour octet. La provenance est un petit fichier qui note d'où vient l'artefact (le commit, l'Id, l'empreinte de l'archive). Promouvoir un artefact, c'est le faire passer d'un environnement au suivant sans le reconstruire.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Constater qu'un rebuild du même Dockerfile donne un autre Id d'image, et en tirer la règle : construire une fois, promouvoir ensuite
- Transférer une archive
docker savevers un second hôte, la vérifier avecsha256sum -c, la charger avecdocker loadet retrouver le même Id - Écrire une base Compose commune et une surcharge par environnement, puis lire la configuration effective avec
docker compose config - Repérer le rebuild caché de
docker compose upquand l'image manque et que le service garde une sectionbuild, et le transformer en erreur avec--no-build --pull never - Séparer deux environnements par un projet (
-p), recréer l'un sans toucher à l'autre - Relever l'Id de l'image en service, le comparer au fichier de provenance, puis lancer une vérification métier avant de promouvoir
Prérequis
Section intitulée « Prérequis »- Développement et déploiement : variantes explicites : la fusion d'une base et d'une surcharge par deux options
-f, et la lecture dedocker compose config. La leçon les emploie sans les réexpliquer. - Automatiser build, tests et publication d'une image : le fichier de provenance qui relie le commit, l'
Idde l'image et l'empreinte de l'archive, avecdocker saveetdocker load. Les deux se comparent ici à ce qui tourne. - Deux hôtes Linux avec Docker Engine 29.9.2 et Compose v5.6.1, un accès SSH à chacun et
curl. L'hôte de build peut aussi faire tourner la recette. Si vos deux hôtes ne se joignent pas entre eux, passez par votre poste comme relais, comme nous. Les deux machines virtuelles du lab proposé en fin de leçon peuvent servir d'hôtes, à condition qu'elles portent ces versions.
Une partie de la leçon (le rebuild caché) télécharge l'image de base python depuis Docker Hub : l'hôte cible doit donc avoir un accès réseau pour la reproduire.
Pourquoi construire une seule fois ?
Section intitulée « Pourquoi construire une seule fois ? »Un rebuild ne redonne pas forcément la même image, même avec les mêmes sources. Le Dockerfile de l'application témoin, un petit serveur « catalogue », écrit l'heure de construction dans l'image : c'est un cas réaliste de contenu qui change à chaque build. Sur l'hôte de build, nous avons construit quatre fois la même chose, avec la même base épinglée, et relevé l'Id de chaque résultat.
| Construction | Id obtenu (12 premiers caractères) |
|---|---|
Première construction, cat-catalogue:1.0 | 5aa54177fc5c |
| Reconstruction avec cache | 75018aed6f00 |
Reconstruction avec --no-cache | c791e168e025 |
Seconde reconstruction avec --no-cache | ca993d2e2fc5 |
Regardez la deuxième ligne : le build avec cache a déjà donné un autre Id, alors que toutes les couches venaient du cache. Nous l'avons constaté avec le magasin containerd de Docker 29.9.2. Le détail compte : en construisant deux fois de suite avec --provenance=false, qui retire l'attestation de provenance ajoutée par défaut à l'image, les deux images ont obtenu le même Id (78affc679754), cache à l'appui. Avec l'attestation par défaut, la reconstruction avec cache en a donné un autre. C'est donc cette attestation qui fait varier l'Id quand le cache est utilisé. Elle décrit comment l'image a été construite et a son utilité : la règle de la leçon n'est pas de la désactiver, mais de ne construire qu'une fois. Retenez qu'un build ne garantit pas le même résultat.
Un environnement qui reconstruit déploie donc un artefact que personne n'a testé. La règle est de construire une fois, puis de déplacer cet artefact tel quel : c'est cela, promouvoir.
Le schéma suivant montre le trajet que parcourt la leçon, de la construction unique au déploiement.
Suivez le trait épais : il mène de la construction unique au déploiement qui garde l'Id, en passant par l'archive, le transfert et le chargement. Le trait pointillé part de l'hôte cible quand l'image y manque : c'est le danger de la section « Pourquoi Compose reconstruit-il sans le dire ? ».
Comment préparer l'application témoin et le dossier de travail ?
Section intitulée « Comment préparer l'application témoin et le dossier de travail ? »Tout se passe dans un dossier ~/c44, créé sur les deux hôtes, que le nettoyage de fin supprime en une fois. Sur l'hôte de build, créez-le avec son sous-dossier app ; sur l'hôte cible, créez seulement ~/c44, le dossier app y arrive par scp un peu plus bas. L'application « catalogue » sert deux routes : /catalogue renvoie son environnement et trois titres, /sante répond ok. À chaque démarrage, elle ajoute une ligne dans /donnees/demarrages.log : c'est la trace qui prouvera que les données d'un volume survivent à une recréation.
mkdir -p ~/c44/app && cd ~/c44Écrivez app/app.py avec ce contenu.
import json, osfrom http.server import BaseHTTPRequestHandler, HTTPServer
ENV = os.environ.get("CAT_ENV", "inconnu")LIVRES = ["Le Petit Prince", "Germinal", "Les Misérables"]
# Chaque démarrage laisse une ligne dans le volume : la preuve que les données survivent.with open("/donnees/demarrages.log", "a") as f: f.write(ENV + "\n")
class H(BaseHTTPRequestHandler): def do_GET(self): if self.path == "/sante": body, code = b"ok\n", 200 elif self.path == "/catalogue": body, code = json.dumps({"env": ENV, "livres": LIVRES}).encode() + b"\n", 200 else: body, code = b"introuvable\n", 404 self.send_response(code) self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body)
HTTPServer(("0.0.0.0", 8000), H).serve_forever()Puis app/Dockerfile. La base porte son digest, et l'étape RUN date fabrique le contenu qui change à chaque build, évoqué plus haut.
FROM python:3.14-alpine@sha256:eb2c62658b8d51bff0acc408504b76521d02030de56a8a3c1e830083e89b89d2RUN date -u +%s%N > /etc/build-stamp && mkdir /donneesCOPY app.py /app/app.pyCMD ["python", "/app/app.py"]Copiez ensuite sur l'hôte cible le dossier app lui-même, avec scp -r app, et non son contenu avec scp -r app/* : ce second geste aplatit le contexte de build, et le build échoue faute de Dockerfile. L'hôte cible n'a pas besoin de ce dossier pour déployer, mais la démonstration du rebuild caché s'en sert plus bas.
Comment transférer l'artefact sans le reconstruire ?
Section intitulée « Comment transférer l'artefact sans le reconstruire ? »Sur l'hôte de build, construisez l'image une seule fois, relevez son Id et exportez-la en archive avec sa somme de contrôle.
docker build -t cat-catalogue:1.0 appdocker image inspect cat-catalogue:1.0 --format "Id={{.Id}}"docker save -o cat-catalogue-1.0.tar cat-catalogue:1.0sha256sum cat-catalogue-1.0.tar | tee cat-catalogue-1.0.tar.sha256Id=sha256:5aa54177fc5c4f47aa3cca02ca7d27d650a9461c694567a55986cfa876c36542bf419e265457a50a497d5e7b208f609aad74a35e23220d2ae352ad423ec6faa0 cat-catalogue-1.0.tarRetenez l'Id : c'est l'identité de l'artefact. Avec le magasin containerd, docker image ls --digests affiche le même sha256 dans les colonnes DIGEST et IMAGE ID (5aa54177fc5c). Autrement dit, l'Id est le digest de l'index OCI, la liste qui décrit l'image pour chacune de ses plateformes. L'archive pèse 17 870 848 octets.
Écrivez ensuite le fichier de provenance, comme dans la leçon sur la CI : il relie le commit, l'Id et l'empreinte de l'archive. Le commit a1b2c3d de notre exemple est fictif.
ID=$(docker image inspect cat-catalogue:1.0 --format '{{.Id}}')SHA=$(cut -d' ' -f1 cat-catalogue-1.0.tar.sha256)printf 'commit=a1b2c3d\nimage=cat-catalogue:1.0\nid=%s\narchive=cat-catalogue-1.0.tar\narchive_sha256=%s\n' "$ID" "$SHA" > provenance.txtcat provenance.txtcommit=a1b2c3dimage=cat-catalogue:1.0id=sha256:5aa54177fc5c4f47aa3cca02ca7d27d650a9461c694567a55986cfa876c36542archive=cat-catalogue-1.0.tararchive_sha256=bf419e265457a50a497d5e7b208f609aad74a35e23220d2ae352ad423ec6faa0Transférez les trois fichiers (l'archive, sa somme de contrôle et provenance.txt) vers ~/c44 de l'hôte cible avec scp. Chez nous, les deux hôtes ne se joignaient pas : les fichiers sont passés par le poste, de l'hôte de build au poste puis du poste à l'hôte cible, et les deux scp ont rendu 0. Sur l'hôte cible, vérifiez l'archive avant de la charger.
sha256sum -c cat-catalogue-1.0.tar.sha256cat-catalogue-1.0.tar: OKLe contrôle doit pouvoir échouer, sinon il ne prouve rien. Nous avons altéré un octet d'une copie de l'archive (en écrivant X à la position 100) et relancé sha256sum -c : il a répondu alteree.tar: FAILED puis sha256sum: WARNING: 1 computed checksum did NOT match, avec le code de retour 1. Ne chargez jamais une archive dont ce contrôle ne rend pas 0 : retransférez-la depuis l'hôte de build, sans reconstruire l'image sur la cible, ce qui produirait un autre artefact. Quand le contrôle répond OK, chargez l'archive.
docker load -i cat-catalogue-1.0.tardocker image inspect cat-catalogue:1.0 --format '{{.Id}}'Loaded image: cat-catalogue:1.0sha256:5aa54177fc5c4f47aa3cca02ca7d27d650a9461c694567a55986cfa876c36542Regardez l'Id : c'est celui de l'hôte de build et celui du fichier de provenance, caractère pour caractère. Comparez-les avec un test plutôt qu'à l'œil, mais testez aussi que les deux valeurs ne sont pas vides.
ATTENDU=$(grep '^id=' provenance.txt | cut -d= -f2)REEL=$(docker image inspect cat-catalogue:1.0 --format '{{.Id}}')[ -n "$ATTENDU" ] && [ "$REEL" = "$ATTENDU" ] && echo "identique à la provenance"Le test -n n'est pas un luxe. Pendant nos mesures, un diff entre deux Id vides, issus d'un build qui avait échoué, a affiché « identique ». Deux valeurs vides sont égales : une comparaison qui ne vérifie pas qu'elle a reçu quelque chose peut réussir à vide.
Un registre de confiance peut remplacer cette archive quand il existe : le digest du manifeste joue alors le rôle de l'Id. Cette variante, avec ses pièges, est traitée dans Cache BuildKit en CI et promotion d'image entre registres, et la leçon ne la rejoue pas.
Comment ne changer que la configuration entre deux environnements ?
Section intitulée « Comment ne changer que la configuration entre deux environnements ? »L'image ne change pas d'un environnement à l'autre, ses réglages si. La base compose.yaml décrit ce qui est commun, avec un image: et aucune section build. Elle ne porte qu'une valeur provisoire pour CAT_ENV. Posez les fichiers de cette section dans ~/c44 ; l'hôte cible n'a besoin que de compose.yaml et de compose.prod.yaml.
services: catalogue: image: cat-catalogue:1.0 environment: CAT_ENV: inconnu volumes: - donnees:/donneesvolumes: donnees: {}Chaque environnement ajoute une surcharge : un second fichier Compose, passé après la base, qui ne porte que ce qui diffère. Ici, ce sont la variable, la limite mémoire et la publication sur la boucle locale (127.0.0.1, joignable depuis l'hôte seulement). Voici compose.recette.yaml, puis compose.prod.yaml. Aucune des deux ne redéfinit image:.
services: catalogue: environment: CAT_ENV: recette mem_limit: 96m ports: - "127.0.0.1:8601:8000"services: catalogue: environment: CAT_ENV: production mem_limit: 192m ports: - "127.0.0.1:8602:8000"Avant de lancer quoi que ce soit, lisez la configuration fusionnée. Les options -f s'appliquent dans l'ordre : la base d'abord, la surcharge ensuite. L'option -p donne son nom au projet Compose, c'est-à-dire à l'ensemble des conteneurs, du réseau et du volume qu'une même commande crée. Ce nom préfixe aussi ceux du réseau et du volume.
docker compose -p cat-recette -f compose.yaml -f compose.recette.yaml configname: cat-recetteservices: catalogue: environment: CAT_ENV: recette image: cat-catalogue:1.0 mem_limit: "100663296" networks: default: null volumes: - type: volume source: donnees target: /donnees volume: {} ports: - mode: ingress host_ip: 127.0.0.1 target: 8000 published: "8601" protocol: tcpnetworks: default: name: cat-recette_defaultvolumes: donnees: name: cat-recette_donneesDans cette sortie, regardez trois choses. image: cat-catalogue:1.0 vient de la base, sans section build. mem_limit est converti en octets (96 Mio donnent "100663296"). Le projet donne ses noms au réseau (cat-recette_default) et au volume (cat-recette_donnees).
La configuration de production, lue de la même façon, donne CAT_ENV: production, "201326592" octets, le port "8602" et des noms en cat-prod_.
Le contrôle qui compte pour une promotion est l'absence de section build dans la configuration fusionnée, pour chaque environnement.
for e in recette prod; do echo -n "$e : lignes build = " docker compose -p cat-$e -f compose.yaml -f compose.$e.yaml config | grep -c builddonerecette : lignes build = 0prod : lignes build = 0Attention au code de retour : grep -c rend 1 quand il compte 0. Ici, 0 est le bon résultat, mais un script qui enchaîne ce grep avec &&, ou qui tourne sous set -e, s'arrête sur un succès. Lisez le compte, pas le code de retour.
La configuration effective dit ce que Compose va demander. Ce qui tourne se lit dans le conteneur, une fois le déploiement fait : la section « Comment séparer deux environnements sur un hôte ? » refait cette vérification sur les conteneurs en service.
Dernier contrôle : la base ne doit pas avoir bougé. Relevez son empreinte avant toute opération avec sha256sum compose.yaml | tee base.sha256, puis revérifiez-la à la fin avec sha256sum -c base.sha256. Dans notre séance, elle est restée identique.
Pourquoi Compose reconstruit-il sans le dire ?
Section intitulée « Pourquoi Compose reconstruit-il sans le dire ? »Quand un service garde une section build et que son image manque sur l'hôte, docker compose up -d la construit sans qu'on le demande. Pour le voir, nous avons posé sur l'hôte cible une troisième surcharge, compose.avec-build.yaml, qui redonne au service son build: ./app, puis supprimé l'image de cet hôte.
services: catalogue: build: ./appdocker rmi cat-catalogue:1.0docker compose -p cat-prod -f compose.yaml -f compose.prod.yaml -f compose.avec-build.yaml up -dLa commande a duré 5,8 secondes et a rendu 0. Elle a affiché Image cat-catalogue:1.0 Built, puis créé le réseau, le volume et le conteneur. L'hôte n'avait pas non plus l'image de base python : pour construire, elle a dû la récupérer, ce qui suppose un accès réseau sur l'hôte. Relevez maintenant l'Id de l'image, puis interrogez l'application.
docker image inspect cat-catalogue:1.0 --format '{{.Id}}'curl -fsS http://127.0.0.1:8602/cataloguesha256:f6cbb17368c4164f41964af1c629650a426e3381eccbae114902a80ab4544f14{"env": "production", "livres": ["Le Petit Prince", "Germinal", "Les Mis\u00e9rables"]}Le tag est toujours cat-catalogue:1.0 et l'application répond correctement, mais l'Id n'est plus 5aa54177, celui de la provenance. Rien n'a alerté : c'est le danger. Un contrôle qui se contente de faire un curl valide cette production, alors qu'elle ne fait pas tourner l'artefact promu.
Le déploiement doit donc échouer au lieu de reconstruire ou de télécharger. Deux options le font : --no-build interdit la construction, --pull never interdit le téléchargement. Sur le même hôte, après avoir arrêté le projet (down -v) et supprimé de nouveau l'image (docker rmi), relancez le déploiement avec la section build conservée :
docker compose -p cat-prod -f compose.yaml -f compose.prod.yaml -f compose.avec-build.yaml \ up -d --no-build --pull neverecho "rc=$?" Network cat-prod_default Creating Network cat-prod_default Creating Volume cat-prod_donnees Creating Volume cat-prod_donnees Creating Volume cat-prod_donnees Created Volume cat-prod_donnees Created Network cat-prod_default Created Network cat-prod_default Created Container cat-prod-catalogue-1 Creating service:catalogue:1 Error response from daemon: No such image: cat-catalogue:1.0Error response from daemon: No such image: cat-catalogue:1.0rc=1Hors d'un terminal, Compose répète certaines lignes de progression : c'est la sortie telle qu'il l'écrit. Lisez bien le message : il ne parle ni de build ni de téléchargement. C'est le démon qui répond No such image quand Compose lui demande de créer le conteneur. Aucun conteneur n'existe après l'échec : docker compose ... ps -a est vide, et docker ps -a --filter label=com.docker.compose.project=cat-prod n'affiche que son en-tête. Mais le réseau cat-prod_default et le volume cat-prod_donnees ont été créés avant, et ils sont toujours là : un échec laisse des restes à nettoyer.
Sur la configuration de promotion, sans section build, nous avons obtenu le même message et le même code 1 avec up -d --no-build --pull never et avec up -d --pull never seul. Sans section build, --pull never suffit donc à empêcher le téléchargement ; --no-build protège le cas où une section build s'est glissée dans la configuration. Gardez les deux options dans vos commandes de déploiement.
Quand l'image est chargée et que la configuration ne porte plus de section build, le même déploiement réussit.
docker load -i cat-catalogue-1.0.tardocker compose -p cat-prod -f compose.yaml -f compose.prod.yaml up -d --no-build --pull neverecho "rc=$?"docker inspect cat-prod-catalogue-1 --format '{{.Image}}'Loaded image: cat-catalogue:1.0 Container cat-prod-catalogue-1 Creating Container cat-prod-catalogue-1 Created Container cat-prod-catalogue-1 Starting Container cat-prod-catalogue-1 Startedrc=0sha256:5aa54177fc5c4f47aa3cca02ca7d27d650a9461c694567a55986cfa876c36542Pour un conteneur, {{.Image}} donne l'Id de l'image dont il est issu. Celui du conteneur de production est bien celui de la provenance : il tourne sur l'artefact promu.
Un dernier point se mesure sur l'hôte de build, une fois la production lancée dessus comme dans la section suivante, avec l'image présente et la section build conservée. Il y faut aussi compose.avec-build.yaml, que nous n'avions posé que sur l'hôte cible : copiez-le. Un up -d sans option ne reconstruit pas (Container ... Running, Id du conteneur et de l'image inchangés). Un up -d --build reconstruit, retague cat-catalogue:1.0 vers un autre Id et recrée le conteneur.
docker compose -p cat-prod -f compose.yaml -f compose.prod.yaml -f compose.avec-build.yaml up -d --build 2>&1 | grep -E 'Built|Recreate|Started'docker inspect cat-prod-catalogue-1 --format 'img={{.Image}}' Image cat-catalogue:1.0 Built Container cat-prod-catalogue-1 Recreate Container cat-prod-catalogue-1 Recreated Container cat-prod-catalogue-1 Startedimg=sha256:55e681386acd8022d34a91e4acedec8d888c5e46f7fca8db31f5eee472ebc9d2Le conteneur de production ne tourne plus sur l'artefact de la provenance, et rien n'a échoué. Une option --build oubliée dans un script de déploiement suffit à changer ce qui tourne. Si vous rejouez cet essai, rétablissez ensuite l'artefact : docker load remet le tag cat-catalogue:1.0 sur l'Id de l'archive, et le déploiement de promotion recrée le conteneur dessus.
docker load -i cat-catalogue-1.0.tardocker compose -p cat-prod -f compose.yaml -f compose.prod.yaml up -d --no-build --pull neverdocker inspect cat-prod-catalogue-1 --format 'prod img={{.Image}}'Loaded image: cat-catalogue:1.0 Container cat-prod-catalogue-1 Recreate Container cat-prod-catalogue-1 Recreated Container cat-prod-catalogue-1 Starting Container cat-prod-catalogue-1 Startedprod img=sha256:5aa54177fc5c4f47aa3cca02ca7d27d650a9461c694567a55986cfa876c36542Comment séparer deux environnements sur un hôte ?
Section intitulée « Comment séparer deux environnements sur un hôte ? »Un projet par environnement sépare conteneurs, réseau et volume. Sur l'hôte de build, nous avons lancé la recette et la production côte à côte avec la même image.
docker compose -p cat-recette -f compose.yaml -f compose.recette.yaml up -d --no-build --pull neverdocker compose -p cat-prod -f compose.yaml -f compose.prod.yaml up -d --no-build --pull neverdocker volume ls --filter name=cat-docker network ls --filter name=cat-DRIVER VOLUME NAMElocal cat-prod_donneeslocal cat-recette_donneesNETWORK ID NAME DRIVER SCOPE9f132ee95d3e cat-prod_default bridge local4c48f51fc32d cat-recette_default bridge localChaque environnement a son volume, son réseau et son port : les deux projets ne partagent ni nom ni donnée, bien qu'ils viennent du même compose.yaml.
Avant de les comparer, vérifiez ce qui tourne réellement. La configuration effective dit ce que Compose a demandé ; docker inspect dit ce que le démon a appliqué. Lisez la limite mémoire (HostConfig.Memory, en octets), la publication de port (HostConfig.PortBindings) et la variable CAT_ENV parmi les variables du conteneur (Config.Env, une liste dont l'image ajoute elle-même plusieurs entrées, d'où le grep).
for e in recette prod; do echo "== $e" docker inspect cat-$e-catalogue-1 --format "Memory={{.HostConfig.Memory}} PortBindings={{json .HostConfig.PortBindings}}" docker inspect cat-$e-catalogue-1 --format "{{range .Config.Env}}{{println .}}{{end}}" | grep "^CAT_ENV="done== recetteMemory=100663296 PortBindings={"8000/tcp":[{"HostIp":"127.0.0.1","HostPort":"8601"}]}CAT_ENV=recette== prodMemory=201326592 PortBindings={"8000/tcp":[{"HostIp":"127.0.0.1","HostPort":"8602"}]}CAT_ENV=productionLes valeurs sont celles des surcharges : 96 Mio donnent 100663296 octets, 192 Mio donnent 201326592, et chaque port n'est publié que sur 127.0.0.1.
Pour vérifier que les données sont séparées, écrivez un fichier note.txt dans le volume de chaque projet et relevez l'identifiant et l'heure de démarrage de chaque conteneur. L'option -T de exec désactive le pseudo-terminal, nécessaire ici parce que la commande reçoit son entrée par un tube.
echo donnee-recette | docker compose -p cat-recette -f compose.yaml -f compose.recette.yaml exec -T catalogue tee /donnees/note.txtecho donnee-prod | docker compose -p cat-prod -f compose.yaml -f compose.prod.yaml exec -T catalogue tee /donnees/note.txtdocker inspect cat-recette-catalogue-1 --format 'recette avant: {{.Id}} {{.State.StartedAt}}'docker inspect cat-prod-catalogue-1 --format 'prod avant: {{.Id}} {{.State.StartedAt}}'donnee-recettedonnee-prodrecette avant: 1e1dde4b4075ed0a3864cc58b476f76fed14eead0b0d4ce4e981d6712d3b09db 2026-11-04T13:04:21.459337784Zprod avant: b2c32451dfdde4a1d5595ebcd961e8497f96a44fe8b5447ada9a4f5eb03baf86 2026-11-04T13:04:21.725411729ZRecréez ensuite la recette seule, puis relevez de nouveau les deux conteneurs.
docker compose -p cat-recette -f compose.yaml -f compose.recette.yaml up -d --force-recreate --no-build --pull neverdocker inspect cat-recette-catalogue-1 --format 'recette apres: {{.Id}} {{.State.StartedAt}}'docker inspect cat-prod-catalogue-1 --format 'prod apres: {{.Id}} {{.State.StartedAt}}' Container cat-recette-catalogue-1 Recreate Container cat-recette-catalogue-1 Recreated Container cat-recette-catalogue-1 Starting Container cat-recette-catalogue-1 Startedrecette apres: 0194798526aa112fd5f78ac925b83cc05a72c81d92f6139296cf04e46fe04f3a 2026-11-04T13:04:42.251887786Zprod apres: b2c32451dfdde4a1d5595ebcd961e8497f96a44fe8b5447ada9a4f5eb03baf86 2026-11-04T13:04:21.725411729ZLe conteneur de la recette a un nouvel identifiant (01947985… au lieu de 1e1dde4b…), alors que celui de la production (b2c32451…) et son StartedAt sont inchangés. Reste à lire les données.
docker compose -p cat-recette -f compose.yaml -f compose.recette.yaml exec -T catalogue cat /donnees/note.txt /donnees/demarrages.logdocker compose -p cat-prod -f compose.yaml -f compose.prod.yaml exec -T catalogue cat /donnees/note.txt /donnees/demarrages.logdonnee-recetterecetterecettedonnee-prodproductionLe note.txt de la recette est intact, son demarrages.log porte deux lignes recette (deux démarrages), et la production en garde une seule, production.
Retenez ce que cela montre : recréer un projet garde son volume et ne touche ni le conteneur ni les données de l'autre. Ce n'est pas le cas de down -v, qui supprime le volume du projet avec ses données ; la leçon ne s'en sert que pour le nettoyage.
Comment tracer une promotion et la valider ?
Section intitulée « Comment tracer une promotion et la valider ? »Pour prouver ce qui tourne, lisez l'Id de l'image de chaque conteneur avec docker inspect et comparez-le à celui de la provenance, avec le même test non vide que plus haut.
ID=$(grep '^id=' provenance.txt | cut -d= -f2)for c in cat-recette-catalogue-1 cat-prod-catalogue-1; do [ -n "$ID" ] && [ "$(docker inspect $c --format '{{.Image}}')" = "$ID" ] \ && echo "$c identique à la provenance" || echo "$c DIFFÉRENT"donecat-recette-catalogue-1 identique à la provenancecat-prod-catalogue-1 identique à la provenanceSi la variable ID est vide (fichier de provenance absent ou mal lu), le test -n échoue et la boucle écrit DIFFÉRENT pour chaque conteneur, au lieu de conclure à tort à l'identité.
Un Id identique prouve que c'est le bon artefact, pas qu'il fonctionne dans cet environnement. La promotion suivante dépend donc d'une vérification métier, c'est-à-dire d'un test de ce que l'application doit faire (ici, servir son catalogue) plutôt que de son seul démarrage. Voici verifier.sh, qui interroge /catalogue, contrôle l'environnement annoncé, compte les titres du catalogue (les chaînes qui commencent par une majuscule) et échoue si le catalogue est vide. Il prend le port et l'environnement attendu. L'option --noproxy '*' de curl l'empêche de passer par un proxy défini dans l'environnement du shell : avec http_proxy=http://127.0.0.1:9 (une adresse où rien n'écoute), un curl sans cette option a échoué avec le code 7, et avec elle il a répondu.
#!/bin/shset -eurep=$(curl -fsS --noproxy '*' --max-time 3 "http://127.0.0.1:$1/catalogue")echo "$rep" | grep -q "\"env\": \"$2\"" || { echo "ECHEC: environnement attendu $2, reçu $rep"; exit 1; }n=$(echo "$rep" | grep -o '"[A-Z][^"]*"' | grep -vc '"env"' || true)[ "$n" -gt 0 ] || { echo "ECHEC: catalogue vide"; exit 1; }echo "OK: $2 répond, catalogue non vide ($n entrées lues)"chmod +x verifier.sh./verifier.sh 8601 recette; echo "rc=$?"./verifier.sh 8601 production; echo "rc=$?"OK: recette répond, catalogue non vide (3 entrées lues)rc=0ECHEC: environnement attendu production, reçu {"env": "recette", "livres": ["Le Petit Prince", "Germinal", "Les Mis\u00e9rables"]}rc=1Le script doit pouvoir échouer, et la seconde commande le prouve : demander production au port de la recette rend le code 1. Il doit aussi refuser un catalogue vide, car une application qui répond sans rien servir n'est pas prête à être promue. Pour le constater, servez un catalogue vide sur un autre port avec le serveur HTTP de Python, qui s'arrête seul au bout de 6 secondes grâce à timeout.
mkdir -p videecho '{"env": "recette", "livres": []}' > vide/cataloguetimeout 6 python3 -m http.server 8603 --bind 127.0.0.1 --directory vide >/dev/null 2>&1 &sleep 1./verifier.sh 8603 recette; echo "rc=$?"waitECHEC: catalogue viderc=1C'est ce code 1 qui bloquerait la promotion suivante. Quand la vérification réussit, consignez la promotion dans un relevé : date, environnement, Id et résultat. L'opérateur && n'écrit la ligne que si verifier.sh a réussi.
res=$(./verifier.sh 8601 recette) && echo "$(date -u +%FT%TZ) recette id=$(docker inspect cat-recette-catalogue-1 --format '{{.Image}}') verification=$res" | tee -a promotions.log2026-11-04T13:04:45Z recette id=sha256:5aa54177fc5c4f47aa3cca02ca7d27d650a9461c694567a55986cfa876c36542 verification=OK: recette répond, catalogue non vide (3 entrées lues)Nous l'avons vérifié dans l'autre sens : avec ./verifier.sh 8601 production à la place, l'affectation a rendu le code 1 et promotions.log n'a pas reçu de seconde ligne.
Ce relevé répond, longtemps après, à la question « qu'est-ce qui a tourné, quand, et avait-on vérifié ? ».
Qui décide de promouvoir ?
Section intitulée « Qui décide de promouvoir ? »La décision de promouvoir reste humaine. Cette leçon montre la mécanique qui garantit que l'artefact promu est le bon ; elle ne dit pas qui a le droit de le promouvoir. Les environnements protégés et les relecteurs obligatoires appartiennent au fournisseur de CI, et s'apprennent dans les formations qui lui sont consacrées. Pour GitHub Actions, la page Promouvoir un artefact : construire une fois, déployer deux fois montre cette approbation autour d'une promotion par digest.
Comment nettoyer ce que la leçon a laissé ?
Section intitulée « Comment nettoyer ce que la leçon a laissé ? »Chaque hôte garde des projets, des images, du cache de construction et le dossier ~/c44. Arrêtez les projets avec -v pour supprimer aussi leurs volumes (la leçon n'en conserve rien), retirez les images, puis supprimez le dossier de travail.
cd ~/c44docker compose -p cat-recette -f compose.yaml -f compose.recette.yaml down -vdocker compose -p cat-prod -f compose.yaml -f compose.prod.yaml down -vdocker rmi cat-catalogue:1.0docker builder prune -fcd ~ && find ~/c44 -deleteSelon l'hôte, certains projets n'existent pas : la recette ne tourne que sur l'hôte de build dans notre séance. Si vous avez construit d'autres étiquettes, retirez-les de la même façon avec docker rmi. L'image de base python n'apparaît pas dans docker image ls -a après les constructions, et un docker rmi sur elle répond No such image : seul le cache de construction en garde la trace. Attention : docker builder prune -f vide le cache de construction de toute la machine, pas seulement celui de la leçon ; sautez-la si vous construisez d'autres images sur cet hôte. Chez nous, docker image ls -a, docker volume ls, docker ps -a et docker system df ne montraient plus rien sur les deux hôtes à la fin.
Mettre en pratique
Section intitulée « Mettre en pratique »Le lab reprend le geste de la leçon sur deux machines virtuelles : une application, un seul artefact, deux déploiements Compose qui ne diffèrent que par leur configuration. Le setup construit l'image sur la première machine, puis charge l'archive de cette image sur la seconde, sans la reconstruire sur la production. Vous écrivez une surcharge par machine, dans le dossier de travail du lab, et vous les fusionnez avec la base par deux options -f.
La validation compare l'identité de l'image préparée aux deux images qui tournent, refuse une clé build dans la configuration fusionnée, vérifie que la base est restée inchangée, lit dans chaque conteneur la variable, la limite mémoire et la publication, puis interroge les deux services. Elle recrée ensuite chaque projet, avec up -d --force-recreate --no-build --pull never, pour constater que ses données survivent et que l'autre n'est pas touché. Le relevé de promotion et la vérification métier vus dans la section « Comment tracer une promotion et la valider ? » restent des gestes à pratiquer à la main : la validation ne les contrôle pas. Elle ne vérifie pas non plus que vous employez --no-build --pull never : elle les emploie elle-même quand elle recrée un environnement.
À retenir
Section intitulée « À retenir »- Un rebuild du même Dockerfile peut donner un autre
Id, même avec le cache (constaté avec le magasin containerd, l'attestation de provenance par défaut en étant la cause) : construire une fois, puis promouvoir cet artefact. - Le transfert par archive suit quatre gestes :
docker save, transfert,sha256sum -c(qui doit rendre 0),docker load. L'Idaprès chargement est celui de l'hôte de build et de la provenance. Comparez-les avec un test qui vérifie aussi qu'ils ne sont pas vides : deux valeurs vides sont égales. - La configuration vit hors de l'image : une base commune sans
build, une surcharge par environnement, fusionnées dans cet ordre.docker compose configdoit montrer zéro lignebuild. - Quand un service garde une section
buildet que l'image manque,docker compose up -dreconstruit sans le dire, sous le même tag mais avec un autreId, et l'application répond quand même : personne ne voit la substitution.--no-build --pull nevertransforme ce rebuild en erreur (No such image, code 1, un message qui ne parle ni de build ni de téléchargement). Etup -d --buildretague l'image et remplace l'artefact. - Un projet par environnement (
-p) donne à chacun son volume et son réseau. Recréer l'un garde ses données et laisse l'autre intact. - Tracez la promotion : l'
Idlu pardocker inspectse compare à la provenance, et une vérification métier réussie dans l'environnement cible précède la promotion suivante. - La décision de promouvoir reste humaine : l'approbation appartient aux formations GitHub Actions et GitLab CI.
- Nettoyez les projets (
down -v), les images, le cache et le dossier~/c44sur les deux hôtes.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de cette leçon est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : Préparer l'exploitation, et reconnaître les limites d'un mono-hôte
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications