Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

Promouvoir le même artefact entre deux environnements

Par mis à jour le

Améliorer cette page

Logo Docker

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.

  • 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 save vers un second hôte, la vérifier avec sha256sum -c, la charger avec docker load et 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 up quand l'image manque et que le service garde une section build, 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
  • Développement et déploiement : variantes explicites : la fusion d'une base et d'une surcharge par deux options -f, et la lecture de docker 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'Id de l'image et l'empreinte de l'archive, avec docker save et docker 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.

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.

ConstructionId obtenu (12 premiers caractères)
Première construction, cat-catalogue:1.05aa54177fc5c
Reconstruction avec cache75018aed6f00
Reconstruction avec --no-cachec791e168e025
Seconde reconstruction avec --no-cacheca993d2e2fc5

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.

Flux de promotion. L'hôte de build construit l'image une fois (Id 5aa54177), écrit un fichier de provenance et une archive docker save accompagnée de sa somme sha256. L'archive est transférée vers l'hôte cible, où sha256sum -c puis docker load redonnent le même Id. Le déploiement avec up -d --no-build --pull never garde cet Id. En pointillés, depuis l'hôte cible : si l'image y manque et que le service garde une section build, up -d reconstruit sans prévenir et produit un autre Id.transfertsection build conservéeHôte de builddocker build, une foisId 5aa54177provenance.txtcommit, Id, empreinteArchive docker save + sha256sumHôte ciblesha256sum -c puis docker loadId 5aa54177up -d --no-build--pull nevermême IdHôte cibleimage absenteup -d avec section buildautre Id : rebuild caché
Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

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.

Fenêtre de terminal
mkdir -p ~/c44/app && cd ~/c44

Écrivez app/app.py avec ce contenu.

import json, os
from 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:eb2c62658b8d51bff0acc408504b76521d02030de56a8a3c1e830083e89b89d2
RUN date -u +%s%N > /etc/build-stamp && mkdir /donnees
COPY app.py /app/app.py
CMD ["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.

Fenêtre de terminal
docker build -t cat-catalogue:1.0 app
docker image inspect cat-catalogue:1.0 --format "Id={{.Id}}"
docker save -o cat-catalogue-1.0.tar cat-catalogue:1.0
sha256sum cat-catalogue-1.0.tar | tee cat-catalogue-1.0.tar.sha256
Id=sha256:5aa54177fc5c4f47aa3cca02ca7d27d650a9461c694567a55986cfa876c36542
bf419e265457a50a497d5e7b208f609aad74a35e23220d2ae352ad423ec6faa0 cat-catalogue-1.0.tar

Retenez 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.

Fenêtre de terminal
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.txt
cat provenance.txt
commit=a1b2c3d
image=cat-catalogue:1.0
id=sha256:5aa54177fc5c4f47aa3cca02ca7d27d650a9461c694567a55986cfa876c36542
archive=cat-catalogue-1.0.tar
archive_sha256=bf419e265457a50a497d5e7b208f609aad74a35e23220d2ae352ad423ec6faa0

Transfé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.

Fenêtre de terminal
sha256sum -c cat-catalogue-1.0.tar.sha256
cat-catalogue-1.0.tar: OK

Le 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.

Fenêtre de terminal
docker load -i cat-catalogue-1.0.tar
docker image inspect cat-catalogue:1.0 --format '{{.Id}}'
Loaded image: cat-catalogue:1.0
sha256:5aa54177fc5c4f47aa3cca02ca7d27d650a9461c694567a55986cfa876c36542

Regardez 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.

Fenêtre de terminal
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:/donnees
volumes:
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.

Fenêtre de terminal
docker compose -p cat-recette -f compose.yaml -f compose.recette.yaml config
name: cat-recette
services:
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: tcp
networks:
default:
name: cat-recette_default
volumes:
donnees:
name: cat-recette_donnees

Dans 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.

Fenêtre de terminal
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 build
done
recette : lignes build = 0
prod : lignes build = 0

Attention 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.

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: ./app
Fenêtre de terminal
docker rmi cat-catalogue:1.0
docker compose -p cat-prod -f compose.yaml -f compose.prod.yaml -f compose.avec-build.yaml up -d

La 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.

Fenêtre de terminal
docker image inspect cat-catalogue:1.0 --format '{{.Id}}'
curl -fsS http://127.0.0.1:8602/catalogue
sha256: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 :

Fenêtre de terminal
docker compose -p cat-prod -f compose.yaml -f compose.prod.yaml -f compose.avec-build.yaml \
up -d --no-build --pull never
echo "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.0
Error response from daemon: No such image: cat-catalogue:1.0
rc=1

Hors 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.

Fenêtre de terminal
docker load -i cat-catalogue-1.0.tar
docker compose -p cat-prod -f compose.yaml -f compose.prod.yaml up -d --no-build --pull never
echo "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 Started
rc=0
sha256:5aa54177fc5c4f47aa3cca02ca7d27d650a9461c694567a55986cfa876c36542

Pour 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.

Fenêtre de terminal
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 Started
img=sha256:55e681386acd8022d34a91e4acedec8d888c5e46f7fca8db31f5eee472ebc9d2

Le 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.

Fenêtre de terminal
docker load -i cat-catalogue-1.0.tar
docker compose -p cat-prod -f compose.yaml -f compose.prod.yaml up -d --no-build --pull never
docker 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 Started
prod img=sha256:5aa54177fc5c4f47aa3cca02ca7d27d650a9461c694567a55986cfa876c36542

Comment 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.

Fenêtre de terminal
docker compose -p cat-recette -f compose.yaml -f compose.recette.yaml up -d --no-build --pull never
docker compose -p cat-prod -f compose.yaml -f compose.prod.yaml up -d --no-build --pull never
docker volume ls --filter name=cat-
docker network ls --filter name=cat-
DRIVER VOLUME NAME
local cat-prod_donnees
local cat-recette_donnees
NETWORK ID NAME DRIVER SCOPE
9f132ee95d3e cat-prod_default bridge local
4c48f51fc32d cat-recette_default bridge local

Chaque 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).

Fenêtre de terminal
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
== recette
Memory=100663296 PortBindings={"8000/tcp":[{"HostIp":"127.0.0.1","HostPort":"8601"}]}
CAT_ENV=recette
== prod
Memory=201326592 PortBindings={"8000/tcp":[{"HostIp":"127.0.0.1","HostPort":"8602"}]}
CAT_ENV=production

Les 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.

Fenêtre de terminal
echo donnee-recette | docker compose -p cat-recette -f compose.yaml -f compose.recette.yaml exec -T catalogue tee /donnees/note.txt
echo donnee-prod | docker compose -p cat-prod -f compose.yaml -f compose.prod.yaml exec -T catalogue tee /donnees/note.txt
docker inspect cat-recette-catalogue-1 --format 'recette avant: {{.Id}} {{.State.StartedAt}}'
docker inspect cat-prod-catalogue-1 --format 'prod avant: {{.Id}} {{.State.StartedAt}}'
donnee-recette
donnee-prod
recette avant: 1e1dde4b4075ed0a3864cc58b476f76fed14eead0b0d4ce4e981d6712d3b09db 2026-11-04T13:04:21.459337784Z
prod avant: b2c32451dfdde4a1d5595ebcd961e8497f96a44fe8b5447ada9a4f5eb03baf86 2026-11-04T13:04:21.725411729Z

Recréez ensuite la recette seule, puis relevez de nouveau les deux conteneurs.

Fenêtre de terminal
docker compose -p cat-recette -f compose.yaml -f compose.recette.yaml up -d --force-recreate --no-build --pull never
docker 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 Started
recette apres: 0194798526aa112fd5f78ac925b83cc05a72c81d92f6139296cf04e46fe04f3a 2026-11-04T13:04:42.251887786Z
prod apres: b2c32451dfdde4a1d5595ebcd961e8497f96a44fe8b5447ada9a4f5eb03baf86 2026-11-04T13:04:21.725411729Z

Le 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.

Fenêtre de terminal
docker compose -p cat-recette -f compose.yaml -f compose.recette.yaml exec -T catalogue cat /donnees/note.txt /donnees/demarrages.log
docker compose -p cat-prod -f compose.yaml -f compose.prod.yaml exec -T catalogue cat /donnees/note.txt /donnees/demarrages.log
donnee-recette
recette
recette
donnee-prod
production

Le 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.

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.

Fenêtre de terminal
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"
done
cat-recette-catalogue-1 identique à la provenance
cat-prod-catalogue-1 identique à la provenance

Si 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/sh
set -eu
rep=$(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)"
Fenêtre de terminal
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=0
ECHEC: environnement attendu production, reçu {"env": "recette", "livres": ["Le Petit Prince", "Germinal", "Les Mis\u00e9rables"]}
rc=1

Le 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.

Fenêtre de terminal
mkdir -p vide
echo '{"env": "recette", "livres": []}' > vide/catalogue
timeout 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=$?"
wait
ECHEC: catalogue vide
rc=1

C'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.

Fenêtre de terminal
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.log
2026-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é ? ».

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.

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.

Fenêtre de terminal
cd ~/c44
docker compose -p cat-recette -f compose.yaml -f compose.recette.yaml down -v
docker compose -p cat-prod -f compose.yaml -f compose.prod.yaml down -v
docker rmi cat-catalogue:1.0
docker builder prune -f
cd ~ && find ~/c44 -delete

Selon 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.

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.

  • 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'Id aprè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 config doit montrer zéro ligne build.
  • Quand un service garde une section build et que l'image manque, docker compose up -d reconstruit sans le dire, sous le même tag mais avec un autre Id, et l'application répond quand même : personne ne voit la substitution. --no-build --pull never transforme ce rebuild en erreur (No such image, code 1, un message qui ne parle ni de build ni de téléchargement). Et up -d --build retague 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'Id lu par docker inspect se 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 ~/c44 sur les deux hôtes.

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

6 questions
6 min.
70% requis

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

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn