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

Par mis à jour le

Améliorer cette page

Logo Docker

Votre Dockerfile commence par FROM alpine:3.24.3, et rien ne garantit que ce tag désigne demain le contenu d'aujourd'hui. Cette leçon, pour qui sait déjà lire l'identité d'une image et publier dans un registre, montre comment écrire dans le FROM l'empreinte exacte du socle, comment savoir qu'une version plus récente est sortie, puis comment livrer l'image finie à une machine qui n'atteint aucun registre public. Deux voies sont mesurées : l'archive, produite par docker save, et un registre dont on transporte le volume. Les sorties viennent de Docker Engine 29.9.2 avec Buildx 0.38.1, sur Debian 13 et le magasin d'images containerd.

  • Relever le digest d'index d'un tag et l'écrire dans un FROM nom:tag@sha256:…
  • Prouver qu'un build épinglé reste sur l'ancien socle quand le tag est déplacé
  • Détecter par un script qu'un socle plus récent est publié
  • Exporter une image avec docker save, savoir ce que --platform change à l'identité de l'image chargée, vérifier l'archive avec sha256sum avant de la charger
  • Livrer une image par un registre dont on transporte le volume, et la tirer par digest sans réseau
  • Expliquer pourquoi un miroir pull-through ne suffit pas hors ligne, et diagnostiquer une livraison qui échoue
  • Lire une image OCI et son identité : tag, index, manifeste et digest. La leçon ne les redéfinit pas.
  • Choisir et maintenir une image de base Docker : pourquoi un tag de socle est mutable et comment suivre un socle dans le temps.
  • Publier et récupérer une image de façon maîtrisée : docker push, docker pull et le registre local en HTTP sur 127.0.0.1.
  • Servir un registre de confiance, et s'y authentifier : la leçon précédente monte un registre avec TLS et un mot de passe. Ici le registre reste en HTTP sur la boucle locale : cela suffit pour la démonstration, pas pour un registre que d'autres machines atteignent.
  • Un Docker Engine 29.9.2 avec Buildx (livré avec lui) sur un hôte amd64, et la possibilité de lancer un conteneur privilégié pour simuler la machine sans réseau. Les tailles et les digests de manifeste de la leçon sont ceux d'un hôte amd64.
  • python3, que la leçon emploie pour lire un jeton et pour altérer une archive volontairement.

Les FROM de la leçon portent leur digest, et les commandes qui lancent alpine:3.24.3 ou busybox:1.37 aussi, sous la forme image:tag@sha256:…. Seules les commandes qui prouvent l'absence d'une image gardent le tag seul : le bloc concerné porte un commentaire # ❌, parce que ces commandes doivent échouer. Les empreintes affichées sont celles de la mesure : celles d'une image que vous construisez vous-même diffèrent des nôtres, ce qui est normal.

Trois comportements ne sont pas garantis, parce que la mesure ne les a pas rejoués : la mise à jour automatique par Renovate, le chargement d'une archive multi-architecture, et un autre magasin que containerd. La leçon les signale au passage. La copie d'un index complet vers un registre par skopeo copy --all n'est pas rejouée dans cette leçon : la page Cache BuildKit en CI et promotion d'image entre registres la mesure, et le guide Skopeo mesure --all vers une archive.

Pour épingler un socle, il faut d'abord lire son empreinte. La commande à retenir est docker buildx imagetools inspect : elle interroge le registre sans tirer l'image et affiche, sans option, le digest de l'index, la liste des variantes par architecture.

Fenêtre de terminal
docker buildx imagetools inspect alpine:3.24.3 | head -12
Name: docker.io/library/alpine:3.24.3
MediaType: application/vnd.oci.image.index.v1+json
Digest: sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
Manifests:
Name: docker.io/library/alpine:3.24.3@sha256:c26612752c78b18faacd63c7d50f4592c2e7b0f25e0e20e82bd74e465c57ab0d
MediaType: application/vnd.oci.image.manifest.v1+json
Platform: linux/amd64
Annotations:
org.opencontainers.image.source: https://github.com/alpinelinux/docker-alpine.git#5aec467fb81238bae9441eea2edb000655d7044f:x86_64
org.opencontainers.image.url: https://hub.docker.com/_/alpine
org.opencontainers.image.version: 3.24.3

Regardez la ligne Digest du haut : c'est celle de l'index, et c'est elle qu'on épingle, pas celle de la première entrée Name, qui est le manifeste linux/amd64. Épingler l'index garde le build portable d'une architecture à l'autre.

Pour n'imprimer que le digest, utilisez --format. La sortie n'a pas de retour à la ligne final, ce qui convient à une affectation de variable.

Fenêtre de terminal
docker buildx imagetools inspect alpine:3.24.3 --format '{{.Manifest.Digest}}'
sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a

Au moment de la mesure, alpine:3.24.3 et alpine:latest renvoyaient le même digest, alpine:3.22 un autre (sha256:2e0b81d6…). Une autre commande, docker manifest inspect, existe toujours, mais elle liste les manifestes de l'index sans afficher le digest de l'index lui-même : elle ne sert pas à relever l'empreinte à épingler.

Comment retrouver ce digest par l'API du registre ?

Section intitulée « Comment retrouver ce digest par l'API du registre ? »

Cette sous-section est facultative. L'API du registre répond à la même question que imagetools : Docker Hub exige un jeton, même anonyme, puis on interroge le manifeste du tag avec un en-tête Accept qui réclame l'index. L'en-tête docker-content-digest de la réponse porte l'empreinte. Le dialogue avec un registre authentifié est détaillé dans la leçon sur le registre de confiance.

Fenêtre de terminal
TOK=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/alpine:pull" | python3 -c 'import sys,json;print(json.load(sys.stdin)["token"])')
curl -sI -H "Authorization: Bearer $TOK" \
-H 'Accept: application/vnd.oci.image.index.v1+json' \
https://registry-1.docker.io/v2/library/alpine/manifests/3.24.3 \
| grep -i -E 'docker-content-digest|^HTTP'
HTTP/2 200
docker-content-digest: sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a

Le digest est le même que celui de imagetools. Ne lancez pas ces commandes avec set -x : le shell afficherait le jeton Bearer.

La forme à écrire est FROM nom:tag@sha256:<digest d'index>. Docker construit alors sur le contenu désigné par le digest. Le tag, lui, ne sert plus qu'à l'humain qui lit le fichier : il dit de quelle version on parle.

FROM alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
RUN cat /etc/alpine-release

Un piège se cache dans cette forme : quand un digest est présent, Docker ignore le tag, et il ne prévient pas. Pour le voir, écrivez un tag faux, 3.22, devant le digest de la 3.24.3 :

FROM alpine:3.22@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
RUN cat /etc/alpine-release

Rangez ce Dockerfile dans un dossier menteur/, comme les autres de la leçon, et construisez-le en désignant ce dossier.

Fenêtre de terminal
docker build --progress=plain --no-cache -t t:menteur menteur
#4 [1/2] FROM docker.io/library/alpine:3.22@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
#4 resolve docker.io/library/alpine:3.22@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a 0.0s done
#5 [2/2] RUN cat /etc/alpine-release
#5 0.151 3.24.3

La construction réussit, et la ligne RUN affiche 3.24.3 : c'est le digest qui a décidé. Le fichier, lui, continue d'annoncer 3.22. Un tag qui ne correspond pas au digest trompe le prochain lecteur sans qu'aucun outil le signale. Après chaque épinglage, vérifiez donc que le tag écrit est bien celui dont vous avez relevé le digest.

Un digest qui n'existe pas n'est pas ignoré, lui. Docker arrête le build et le dit :

ERROR: failed to build: failed to solve: alpine@sha256:0000000000000000000000000000000000000000000000000000000000000000: failed to resolve source metadata for docker.io/library/alpine@sha256:0000000000000000000000000000000000000000000000000000000000000000: docker.io/library/alpine@sha256:0000000000000000000000000000000000000000000000000000000000000000: not found

Le message se lit de droite à gauche : not found est la réponse du registre, failed to resolve source metadata dit que le socle n'a pas pu être résolu, et l'erreur de build en découle. Une faute de frappe dans le digest se repère donc tout de suite.

Que garde un build épinglé quand le tag est déplacé ?

Section intitulée « Que garde un build épinglé quand le tag est déplacé ? »

Un build épinglé reste sur l'ancien contenu, même si le tag désigne désormais autre chose. Pour le prouver sans attendre qu'un registre public bouge, il faut un tag que vous déplacez vous-même. Un registre local sur la boucle locale suffit, comme dans la leçon sur Docker Hub : l'adresse 127.0.0.1 est acceptée en HTTP sans réglage.

Fenêtre de terminal
docker volume create livraison-vol
docker run -d --name registre-livraison -p 127.0.0.1:5001:5000 -v livraison-vol:/var/lib/registry registry:3.1.2@sha256:d4032d4cc323c1731c707931ecada07a53076eac38e3b61ae57533f170f2c6c3

Construisez un premier socle maison, nommé socle-maison:2026-11, qui ajoute un fichier /v contenant version-1. Rangez ce Dockerfile dans un dossier v1/, le suivant dans v2/ et celui de l'application dans app/. Poussez le socle, puis relevez son digest dans la variable OLD.

FROM alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
RUN echo version-1 > /v
Fenêtre de terminal
docker build -q -t 127.0.0.1:5001/socle-maison:2026-11 v1 >/dev/null && docker push 127.0.0.1:5001/socle-maison:2026-11 | tail -2
OLD=$(docker buildx imagetools inspect 127.0.0.1:5001/socle-maison:2026-11 --format '{{.Manifest.Digest}}'); echo "OLD=$OLD"
f27d9101980d: Pushed
2026-11: digest: sha256:68dc2aea13813b3242bdff68332dd2e6c9dc136b15298d22a461e48b6ca112fe size: 855
OLD=sha256:68dc2aea13813b3242bdff68332dd2e6c9dc136b15298d22a461e48b6ca112fe

Refaites la même chose avec un second Dockerfile, rangé dans v2/, qui écrit version-2 dans /v, sous le même tag : c'est le déplacement. Le registre accepte le push, et le tag pointe maintenant vers un autre digest, que vous relevez dans NEW.

Fenêtre de terminal
docker build -q -t 127.0.0.1:5001/socle-maison:2026-11 v2 >/dev/null && docker push 127.0.0.1:5001/socle-maison:2026-11 | tail -2
NEW=$(docker buildx imagetools inspect 127.0.0.1:5001/socle-maison:2026-11 --format '{{.Manifest.Digest}}'); echo "NEW=$NEW"
7b229517ed22: Layer already exists
2026-11: digest: sha256:6e8876d4f66de01da0b1744bfc0878f8993c858f115f6f4fe84fe42a0fe98772 size: 855
NEW=sha256:6e8876d4f66de01da0b1744bfc0878f8993c858f115f6f4fe84fe42a0fe98772

Écrivez maintenant l'application sur le socle, avec le digest de l'ancien contenu, supprimez l'image locale du socle pour forcer la résolution, puis construisez en demandant au moteur de revérifier le registre (--pull) et de ne rien réutiliser (--no-cache).

FROM 127.0.0.1:5001/socle-maison:2026-11@sha256:68dc2aea13813b3242bdff68332dd2e6c9dc136b15298d22a461e48b6ca112fe
RUN cat /v > /vu
CMD ["cat","/vu"]
Fenêtre de terminal
docker image rm 127.0.0.1:5001/socle-maison:2026-11
docker build --pull --no-cache -t demo-web:1.0 app 2>&1 | grep -E 'FROM|ERROR|resolve'
docker run --rm demo-web:1.0
#4 [1/2] FROM 127.0.0.1:5001/socle-maison:2026-11@sha256:68dc2aea13813b3242bdff68332dd2e6c9dc136b15298d22a461e48b6ca112fe
#4 resolve 127.0.0.1:5001/socle-maison:2026-11@sha256:68dc2aea13813b3242bdff68332dd2e6c9dc136b15298d22a461e48b6ca112fe 0.0s done
version-1

Regardez la dernière ligne : le conteneur affiche version-1, alors que le tag désigne version-2. Le build est resté sur le socle épinglé. Le registre sert toujours l'ancien digest : une requête sur manifests/<ancien digest> répond 200. L'épinglage garantit le contenu, pas sa disponibilité : si le registre cesse un jour de servir ce digest, le build échoue avec l'erreur not found vue plus haut, et c'est préférable à un contenu qui change en silence.

Comment savoir qu'un socle plus récent est publié ?

Section intitulée « Comment savoir qu'un socle plus récent est publié ? »

Un socle épinglé vieillit, et c'est voulu : vous décidez quand remonter. Il faut seulement que quelque chose vous prévienne qu'une remontée est possible. Un script de quelques lignes compare le digest épinglé dans le Dockerfile au digest que le registre sert maintenant pour le tag, sans outil tiers. Enregistrez-le sous le nom verifie-socle.sh.

#!/usr/bin/env bash
# Compare le digest épinglé dans le Dockerfile au digest que le registre sert pour le tag.
set -euo pipefail
ref=$(awk '/^FROM /{print $2; exit}' "$1")
name=${ref%@*}; pinned=${ref#*@}
served=$(docker buildx imagetools inspect "$name" --format '{{.Manifest.Digest}}')
if [ "$served" = "$pinned" ]; then echo "à jour : $name"; else echo "SOCLE PLUS RÉCENT : $name sert $served, épinglé $pinned"; exit 1; fi

Le script lit la première ligne FROM du fichier passé en argument. Il sépare la référence en deux avec ${ref%@*}, qui garde le nom et le tag, et ${ref#*@}, qui garde le digest. Puis il demande au registre quel digest il sert pour nom:tag.

C'est la raison de garder le tag dans le FROM : sans lui, le script n'aurait aucun nom à interroger, et le digest épinglé ne dirait rien sur la suite de la branche.

Le script a des limites. Il ne lit que la première ligne FROM : un Dockerfile multi-étapes demande un script par socle. Un FROM --platform=... lui fait lire l'option à la place de la référence. Et un registre injoignable, ou une limite de pulls atteinte, rend aussi rc=1 : ce code dit « le contrôle a échoué », pas forcément « un socle plus récent existe ». Lisez le message avant de conclure.

Appliqué à l'application de la section précédente, dont le tag a été déplacé :

Fenêtre de terminal
bash verifie-socle.sh app/Dockerfile; echo "rc=$?"
SOCLE PLUS RÉCENT : 127.0.0.1:5001/socle-maison:2026-11 sert sha256:6e8876d4f66de01da0b1744bfc0878f8993c858f115f6f4fe84fe42a0fe98772, épinglé sha256:68dc2aea13813b3242bdff68332dd2e6c9dc136b15298d22a461e48b6ca112fe
rc=1

Le message nomme les deux empreintes, et le code de retour vaut 1. C'est lui qui fait échouer une étape de pipeline : une tâche planifiée qui lance le script cesse d'être verte dès qu'un socle plus récent est publié, et personne n'a besoin de lire un journal pour le savoir.

Le script ne dit pas si la nouvelle version est bonne pour vous, il dit seulement que le socle a bougé ; la décision de remonter, avec les contrôles de l'application, est celle de la leçon sur le choix d'un socle.

Des outils automatisent la même veille et ouvrent la demande de mise à jour à votre place. Le guide sur Renovate et la page sur le maintien à l'échelle des références épinglées en traitent. D'après sa documentation, Renovate épingle par digest : il garde le tag dans la ligne FROM et propose une mise à jour quand le contenu du tag change.

Un digest figé ne reçoit aucun correctif de sécurité : l'image épinglée garde ses vulnérabilités tant que vous ne remontez pas le socle. L'épinglage rend la mise à jour volontaire, il ne la supprime pas, et le script de détection ci-dessus sert à ne pas laisser un socle vieillir sans que quelqu'un l'ait décidé.

Archive ou registre : quelle voie pour livrer hors ligne ?

Section intitulée « Archive ou registre : quelle voie pour livrer hors ligne ? »

Une machine sans accès à Internet ne peut pas faire docker pull sur Docker Hub. Il faut donc lui apporter l'image par un autre chemin, physique ou interne. Le support de transfert (clé USB, partage réseau interne) change d'un site à l'autre ; la leçon s'intéresse à ce qu'on transporte. Deux voies existent, et elles diffèrent sur ce point.

Le schéma ci-dessous montre les deux chaînes côte à côte. Regardez les deux cases marquées sha256sum -c : dans les deux voies, la somme se vérifie avant tout chargement, et le reçu voyage par un canal séparé des archives. La voie de gauche transporte un fichier que docker load consomme, produit par docker save. La voie de droite transporte un registre entier, son image et son volume, qu'on relance sur la machine isolée avant d'y tirer l'image par son digest.

Deux chaînes depuis une machine connectée vers une machine isolée. Voie 1, l'archive : docker save écrit une archive et son reçu sha256sum, l'archive est transférée et le reçu arrive par un canal séparé, sha256sum -c vérifie l'archive, puis docker load la charge. Voie 2, le registre transporté : docker push envoie l'image dans un registre local, le registre est arrêté et son volume et son image sont archivés, les archives sont transférées et leur reçu arrive par un canal séparé, sha256sum -c les vérifie, l'image registry est chargée, le volume restauré, le registre relancé et l'image tirée par digest. Les deux voies finissent par docker run avec --pull never sur la machine isolée.Voie 2 : le registre transportéVoie 1 : l'archiveMachine connectéeimage construite ou tiréedocker save, puissha256sum écrit le reçuTransfert du .tar ;reçu par un canal séparésha256sum -cavant tout chargementdocker loaddocker pushvers un registre localArrêt du registre,archive de son volumeet de l'image registryTransfert des archives ;reçu par un canal séparésha256sum -cavant tout chargementdocker load de l'image registry,volume restauré, registre relancédocker pull nom@digestMachine isoléedocker run --pull never
Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

L'archive est la voie la plus courte quand on livre une ou deux images vers une machine. Le registre transporté demande plus de gestes, et il convient quand plusieurs machines isolées tirent les mêmes images, ou qu'un outil attend une référence de registre plutôt qu'un fichier.

Pour vérifier une livraison, il faut une machine qui ne peut vraiment rien joindre. Si vous n'en avez pas sous la main, un conteneur Docker-dans-Docker démarré avec --network none en joue le rôle. C'est un moyen de démonstration, pas une méthode de livraison : l'option --privileged donne à ce conteneur presque tous les droits sur la machine hôte, et on ne la pose que sur un poste de test.

Fenêtre de terminal
mkdir -p xfer
docker run -d --name machine-isolee --privileged --network none \
-e DOCKER_TLS_CERTDIR= -v "$PWD/xfer:/xfer:ro" \
docker:dind@sha256:9e79b879b177fbd72c34376a3dd3d91747da5db49e1f560f76aea59561471977
until docker exec machine-isolee docker info >/dev/null 2>&1; do sleep 2; done
docker exec machine-isolee docker version --format 'server {{.Server.Version}}'
docker exec machine-isolee docker info --format 'driver={{.Driver}} status={{.DriverStatus}} images={{.Images}}'
server 29.9.2
driver=overlayfs status=[[driver-type io.containerd.snapshotter.v1]] images=0

La boucle until attend que le démon interne réponde : il lui faut environ 18 secondes après le lancement du conteneur, et un docker exec … docker version lancé aussitôt échoue avec failed to connect to the docker API. L'option -e DOCKER_TLS_CERTDIR= désactive le TLS entre le client et le démon interne : sans risque ici, puisque --network none ferme le conteneur au réseau, et cela évite de gérer des certificats. Le dossier xfer de l'hôte est monté en lecture seule : il joue le support de transfert. Le démon interne est lui aussi un Docker 29.9.2 sur le magasin containerd, avec zéro image.

Vérifiez maintenant que l'isolement est réel, en demandant à ce conteneur de joindre Docker Hub :

Fenêtre de terminal
# ❌ Par le tag, à dessein : ces trois commandes prouvent l'absence de l'image, pas son identité
docker exec machine-isolee wget -T 3 -qO- https://registry-1.docker.io/v2/
docker exec machine-isolee docker pull alpine:3.24.3
docker exec machine-isolee docker run --pull never --rm alpine:3.24.3 true
wget: bad address 'registry-1.docker.io'
Error response from daemon: failed to resolve reference "docker.io/library/alpine:3.24.3": failed to do request: Head "https://registry-1.docker.io/v2/library/alpine/manifests/3.24.3": dial tcp: lookup registry-1.docker.io on 10.10.114.1:53: dial udp 10.10.114.1:53: connect: network is unreachable
docker: Error response from daemon: No such image: alpine:3.24.3
Run 'docker run --help' for more information

Trois messages se lisent ici. bad address dit que le nom du registre ne se résout même pas. L'erreur du pull finit par network is unreachable : c'est l'échec attendu hors ligne, le même par tag et par digest.

Le troisième change la nature de l'échec. Avec --pull never, Docker ne cherche plus de registre et répond No such image, parce que l'image n'est pas dans le magasin local. Cette commande sert de preuve : si elle fonctionne après le chargement, l'image est bien arrivée.

Pour tout le reste de la leçon, les commandes préfixées par docker exec machine-isolee se lancent sur la machine isolée. Sur une vraie machine, retirez ce préfixe.

docker save écrit une archive tar de l'image, avec ses couches et ses métadonnées. Tirez l'image par son digest, redonnez-lui son tag avec docker tag, puis exportez-la : -o nomme le fichier de sortie. Le docker tag n'est pas décoratif, car un pull par digest ne pose pas le tag, et la section sur docker pull tag@digest montre plus bas ce qui arrive sans lui.

Fenêtre de terminal
docker pull alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
docker tag alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a alpine:3.24.3
docker save -o xfer/alpine.tar alpine:3.24.3
ls -l xfer/alpine.tar
tar tf xfer/alpine.tar
-rw------- 1 ansible ansible 3961856 Nov 3 19:03 xfer/alpine.tar
blobs/
blobs/sha256/
blobs/sha256/3da1513d2124248e6e2a526d532ff365833915ce1abe0813bfc5344190827406
blobs/sha256/3ec36f3670df56d1b1d4323910cf6747576cb31e5e5d32c64715339876d86f20
blobs/sha256/7b229517ed222597277036d94892173207d01a731ab3a88eeb1de2e963a2c788
blobs/sha256/964745728d174226819257152a79b34561fe7321472eb9c8e32f461cbc7969c2
blobs/sha256/b92841763c5c2140a20eeb2a44e94ebb0e80fd51d9d6b5895ea72fa999525d68
blobs/sha256/bcd0699075b7694fe6061b0cf0bc8134b3f900f8d7456021c06b7cbbef460f8b
blobs/sha256/be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
blobs/sha256/c26612752c78b18faacd63c7d50f4592c2e7b0f25e0e20e82bd74e465c57ab0d
index.json
manifest.json
oci-layout

L'archive pèse environ 4 Mo, et deux choses se lisent dans cette sortie. Les droits sont -rw------- : seul le compte qui a créé l'archive peut la lire, donc un autre compte chargé du transfert devra d'abord obtenir les droits. Le dossier blobs/sha256/ contient des fichiers nommés par leur empreinte, dont be2656…, l'index, et c26612…, le manifeste amd64 : l'archive embarque la même structure que le registre.

Pour livrer plusieurs images d'un coup, docker save accepte plusieurs noms dans la même commande. La section suivante le fait avec alpine:3.24.3 et busybox:1.37, une fois busybox:1.37 tirée : l'archive deux.tar pèse 6 192 640 octets, soit environ la somme des deux archives séparées, et elle se charge en une seule commande sur la machine isolée.

Pourquoi docker pull tag@digest ne pose-t-il pas le tag ?

Section intitulée « Pourquoi docker pull tag@digest ne pose-t-il pas le tag ? »

Si vous tirez une image par tag et par digest (docker pull busybox:1.37@sha256:…), Docker récupère le contenu mais ne crée pas le tag dans le magasin local. Le tag est ignoré, exactement comme dans un FROM : seul le digest compte. Le piège apparaît au moment d'exporter.

Fenêtre de terminal
docker pull -q busybox:1.37@sha256:a7dbc319b1438115a207e11930dc4bb3e643dcb3ec0bdd694bb36cd8fad289ec
docker images --digests busybox
docker save -o xfer/bb.tar busybox:1.37
docker.io/library/busybox:1.37@sha256:a7dbc319b1438115a207e11930dc4bb3e643dcb3ec0bdd694bb36cd8fad289ec
REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
busybox <none> sha256:a7dbc319b1438115a207e11930dc4bb3e643dcb3ec0bdd694bb36cd8fad289ec a7dbc319b143 2 years ago 6.77MB
Error response from daemon: No such image: busybox:1.37

La colonne TAG affiche <none> : l'image est là, mais sans nom. docker save busybox:1.37 échoue donc avec No such image, ce qui surprend quand on vient de la tirer. La parade tient en une commande, qui pose le tag sur l'image déjà présente :

Fenêtre de terminal
docker tag busybox@sha256:a7dbc319b1438115a207e11930dc4bb3e643dcb3ec0bdd694bb36cd8fad289ec busybox:1.37
docker save -o xfer/deux.tar alpine:3.24.3 busybox:1.37
docker save -o xfer/bb.tar busybox:1.37

L'archive bb.tar ne contient que busybox:1.37 : la section sur le reçu s'en sert. Le même réflexe vaut pour l'image registry de la voie par registre : tirée par digest, elle doit être retaguée avant d'être exportée.

docker save sans option exporte toutes les plateformes dont le contenu est présent dans le magasin local, pas toutes celles que l'index annonce. Ce comportement est celui que décrit la documentation de docker image save. Sur un hôte amd64, un docker pull busybox:1.37@sha256:… ne télécharge que la variante amd64 : l'archive de docker save -o xfer/bb.tar busybox:1.37 pèse alors 2 235 392 octets, et son index.json décrit tout l'index alors que seul le manifeste amd64 est dans l'archive.

Tirez ensuite l'autre variante, puis exportez vers un fichier distinct de bb.tar, que la section sur le reçu réutilise.

Fenêtre de terminal
docker pull -q --platform linux/arm64 busybox@sha256:a7dbc319b1438115a207e11930dc4bb3e643dcb3ec0bdd694bb36cd8fad289ec
docker save -o xfer/bb-multi.tar busybox:1.37
ls -l xfer/bb-multi.tar

La même commande produit alors 4 144 640 octets, avec les manifestes amd64 et arm64, que vous exportiez le tag ou la référence busybox@sha256:….

L'option --platform change ce qui est exporté, et elle change aussi l'identité de l'image chargée. Sans elle, l'archive garde l'index : son index.json désigne l'index (be2656… pour alpine:3.24.3), exactement comme dans l'archive de la section précédente. Avec --platform, l'archive est réduite à un seul manifeste : son index.json désigne le manifeste c26612…, et le blob de l'index be2656… n'y figure plus.

Fenêtre de terminal
docker save --platform linux/amd64 -o xfer/alpine-amd64.tar alpine:3.24.3
docker save --platform linux/arm64 -o xfer/alpine-arm64.tar alpine:3.24.3
Error response from daemon: no suitable export target found: image with reference alpine:3.24.3 was found but does not provide the specified platform (linux/arm64)

La première commande réussit (3 952 640 octets, contre 3 961 856 pour l'archive sans option). La seconde échoue parce que la variante arm64 d'alpine n'a jamais été téléchargée sur cette machine : --platform choisit parmi ce que le magasin local possède, il ne va pas la chercher. Après un docker pull --platform linux/arm64 de busybox, docker save --platform linux/arm64 -o xfer/bb-arm.tar busybox:1.37 fonctionne et produit une archive de 1 915 392 octets.

Une archive faite avec --platform ressemble donc à ce que reçoit un registre après un docker push d'une seule plateforme : une image dont le digest est celui du manifeste, pas celui de l'index. La section suivante montre ce que cela change sur la machine isolée.

Retenez la conséquence pour une livraison. Sachez pour quelle architecture est la machine isolée, et exportez sans --platform si vous épinglez vos FROM sur le digest d'index : l'archive garde l'index, et exporte les plateformes présentes dans le magasin. Une archive qui couvre plusieurs architectures s'obtient en tirant chaque variante avant l'export. Exportez avec --platform seulement pour réduire l'archive, et acceptez alors que l'image chargée porte le digest du manifeste. La réduction ne vaut que si le magasin local porte plusieurs plateformes : avec la seule plateforme amd64 d'alpine, l'écart n'est que de 9 216 octets (3 952 640 contre 3 961 856). Le chargement d'une archive multi-architecture n'a pas été rejoué dans cette leçon. La construction des variantes est celle de la leçon sur le multi-architecture.

Que préserve docker load sur la machine isolée ?

Section intitulée « Que préserve docker load sur la machine isolée ? »

Sur la machine isolée, on compare d'abord l'empreinte de l'archive à celle qui est partie, puis on la charge. Dans la mesure, sha256sum xfer/alpine.tar sur la machine connectée et sha256sum /xfer/alpine.tar sur la machine isolée donnent la même somme, 6fdb2c195eee3b0c1ccf5198c506c3c1ec8a360529c374849d6cc655404b191c. Ici la comparaison se fait à l'œil ; la section sur la vérification de l'archive l'automatise avec un reçu et sha256sum -c.

Fenêtre de terminal
docker exec machine-isolee sha256sum /xfer/alpine.tar
docker exec machine-isolee docker load -i /xfer/alpine.tar
docker exec machine-isolee docker images --digests
docker exec machine-isolee docker image inspect alpine:3.24.3 --format 'Id={{.Id}} RepoDigests={{.RepoDigests}}'
docker exec machine-isolee docker run --rm --pull never alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a cat /etc/alpine-release
docker exec machine-isolee docker run --rm --pull never alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a cat /etc/alpine-release
6fdb2c195eee3b0c1ccf5198c506c3c1ec8a360529c374849d6cc655404b191c /xfer/alpine.tar
Loaded image: alpine:3.24.3
REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
alpine 3.24.3 sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a be2656061b53 12 days ago 13MB
Id=sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a RepoDigests=[alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a]
3.24.3
3.24.3

Regardez trois points. Le tag arrive avec l'image : Loaded image le nomme. L'identifiant Id est le digest de l'index, celui que vous aviez épinglé, et RepoDigests le confirme : sur ce magasin containerd, le chargement d'une archive faite sans --platform ne change pas l'identité. Enfin, l'image se lance avec --pull never, donc sans aucun accès réseau, par son tag épinglé (tag@sha256:…) comme par son digest seul.

Une image construite localement se comporte de même : son Id, ses RepoDigests et ses couches (RootFS.Layers) sont identiques avant docker save et après docker load. Cette identité préservée vaut pour le magasin containerd de la leçon, et pour une archive faite sans --platform. La leçon sur l'export sans démon avec Skopeo montre une voie qui change cette identité.

Que devient l'identité d'une archive faite avec --platform ?

Section intitulée « Que devient l'identité d'une archive faite avec --platform ? »

Chargez l'archive alpine-amd64.tar de la section précédente sur la machine isolée, par-dessus l'image de alpine.tar déjà chargée : les deux portent le nom alpine:3.24.3, donc la nouvelle archive remplace l'image existante (comportement mesuré avec une archive de Skopeo dans la leçon sur l'export sans démon avec Skopeo ; la sortie ci-dessous vient d'une machine vide). Lisez ensuite l'identité de l'image et tentez de la lancer par le digest d'index.

Fenêtre de terminal
docker exec machine-isolee docker load -i /xfer/alpine-amd64.tar
docker exec machine-isolee docker image inspect alpine:3.24.3 --format 'Id={{.Id}} RepoDigests={{.RepoDigests}}'
docker exec machine-isolee docker run --rm --pull never alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a cat /etc/alpine-release
docker exec machine-isolee docker run --rm --pull never alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a cat /etc/alpine-release
Loaded image: alpine:3.24.3
Id=sha256:c26612752c78b18faacd63c7d50f4592c2e7b0f25e0e20e82bd74e465c57ab0d RepoDigests=[alpine@sha256:c26612752c78b18faacd63c7d50f4592c2e7b0f25e0e20e82bd74e465c57ab0d]
docker: Error response from daemon: No such image: alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
Run 'docker run --help' for more information
docker: Error response from daemon: No such image: alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
Run 'docker run --help' for more information

Regardez l'Id : c'est le digest du manifeste amd64 (c26612…), et non celui de l'index (be2656…). Les deux lancements par le digest d'index échouent avec No such image (code 125), alors que l'image est bien chargée : elle porte simplement un autre digest.

Un build dont le FROM est épinglé sur l'index échoue de la même façon, avec une erreur qui ressemble à une panne de réseau, parce que Docker cherche alors le digest d'index sur le registre :

#2 ERROR: failed to do request: Head "https://registry-1.docker.io/v2/library/alpine/manifests/sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a": dial tcp: lookup registry-1.docker.io on 10.10.114.1:53: dial udp 10.10.114.1:53: connect: network is unreachable

Avec une archive faite sans --platform (alpine.tar), le même build réussit, comme les deux lancements --pull never. Le choix se résume donc à ceci : sans --platform, le FROM épinglé sur l'index se résout ; avec --platform, il faut épingler le FROM sur le digest du manifeste, celui que la première entrée Name de docker buildx imagetools inspect affiche, et dont l'archive porte le contenu.

Le chargement de l'archive --platform a donc remplacé l'image venue de alpine.tar : une archive chargée sous un nom déjà présent prend la place de celle qui le portait. Les sections qui suivent s'appuient sur le digest d'index : retirez cette image et rechargez l'archive faite sans option.

Fenêtre de terminal
docker exec machine-isolee docker rmi -f alpine:3.24.3
docker exec machine-isolee docker load -i /xfer/alpine.tar
Untagged: alpine:3.24.3
Deleted: sha256:c26612752c78b18faacd63c7d50f4592c2e7b0f25e0e20e82bd74e465c57ab0d
Loaded image: alpine:3.24.3

Le code de retour de docker load ne prouve pas que l'archive est intacte. Une archive altérée d'un seul octet, de même taille, se charge avec le code 0, et l'image apparaît dans docker images. La documentation de docker image load ne décrit aucune vérification d'intégrité : la somme de contrôle, calculée avant le départ et vérifiée avant le chargement, est le contrôle fiable contre une archive abîmée (sa limite face à une altération volontaire est donnée plus bas).

Sur la machine connectée, écrivez le reçu : ici dans xfer, pour la démonstration. Sur la machine isolée, vérifiez-le avant tout docker load.

Fenêtre de terminal
(cd xfer && sha256sum bb.tar > bb.tar.sha256 && sha256sum -c bb.tar.sha256)
bb.tar: OK

Pour voir ce que la vérification attrape, corrigez un octet d'une couche dans une copie de l'archive. Le script extrait l'archive, change l'octet 1000 d'une couche, puis réarchive. Le dossier extrait est en lecture seule, d'où le chmod. Comme busybox est tiré par digest, la couche modifiée (68fe9bff…) porte chez vous le même nom que dans la mesure.

Fenêtre de terminal
mkdir -p alt && cd alt && tar xf ../xfer/bb.tar && cd ..
chmod -R u+w alt
python3 - <<'PY'
p="alt/blobs/sha256/68fe9bff2ad4ad46da3e58db03faa64eb73c0ff16d546b0c42539eaa9854cb7f"
b=bytearray(open(p,"rb").read()); b[1000]^=0xff; open(p,"wb").write(b)
PY
(cd alt && tar cf ../xfer/alteree.tar .)
(cd xfer && cp bb.tar.sha256 alteree.tar.sha256 && sed -i 's/ bb.tar/ alteree.tar/' alteree.tar.sha256 && sha256sum -c alteree.tar.sha256; echo rc=$?)
alteree.tar: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match
rc=1

La somme refuse l'archive. Voyez maintenant ce que fait docker load sur la machine isolée si on lui donne quand même cette archive. La sortie montrée suppose que busybox:1.37 n'a pas encore été chargé sur cette machine, ce qui est le cas à ce stade de la leçon, où seule alpine y a été chargée ; avec busybox déjà présent, le résultat n'a pas été mesuré.

Fenêtre de terminal
docker exec machine-isolee docker load -i /xfer/alteree.tar; echo rc=$?
docker exec machine-isolee docker run --rm --pull never busybox:1.37@sha256:a7dbc319b1438115a207e11930dc4bb3e643dcb3ec0bdd694bb36cd8fad289ec echo ok
Loaded image: busybox:1.37
Error unpacking image busybox:1.37: apply layer error for "docker.io/library/busybox:1.37": failed to extract layer sha256:5a9bfd996c6d49c6c3cb4ddfb76e8d09cf16b6b7cf6601d1f2dffbdbc3edb5d4: NotFound: failed to get reader from content store: content digest sha256:68fe9bff2ad4ad46da3e58db03faa64eb73c0ff16d546b0c42539eaa9854cb7f: not found
rc=0
docker: Error response from daemon: apply layer error for "": failed to extract layer sha256:5a9bfd996c6d49c6c3cb4ddfb76e8d09cf16b6b7cf6601d1f2dffbdbc3edb5d4: NotFound: failed to get reader from content store: content digest sha256:68fe9bff2ad4ad46da3e58db03faa64eb73c0ff16d546b0c42539eaa9854cb7f: not found
Run 'docker run --help' for more information

Le chargement affiche Loaded image puis une erreur d'extraction, et rend rc=0. L'image est listée, mais docker run échoue. Le message content digest … not found montre que le magasin a refusé le blob altéré : un contrôle par empreinte existe bel et bien, mais son échec ne passe pas dans le code de retour de docker load. Le reçu reste donc nécessaire, et il aurait évité de découvrir le problème sur la machine de production.

Le reçu protège d'une archive abîmée. Contre une altération volontaire, il n'a de valeur que s'il voyage par un autre canal que l'archive elle-même : un attaquant qui change l'archive change aussi un reçu rangé à côté. Pour une garantie d'origine, la signature de l'image va plus loin, comme le montre le guide sur Cosign.

Deux autres échecs se reconnaissent à leur message. Une archive tronquée (ici 2 000 000 octets d'une archive qui en comptait plus) se refuse nettement, avec un code non nul :

failed to ingest "blobs/sha256/7b229517ed222597277036d94892173207d01a731ab3a88eeb1de2e963a2c788": short read: expected 3853109 bytes but got 1911424: unexpected EOF

Et une archive de la mauvaise architecture se charge sans erreur. L'échec arrive au lancement : docker run avertit que la plateforme de l'image (linux/arm64/v8) ne correspond pas à celle de l'hôte (linux/amd64/v3), puis le premier programme lancé échoue.

WARNING: The requested image's platform (linux/arm64/v8) does not match the detected host platform (linux/amd64/v3) and no specific platform was requested
exec /bin/uname: exec format error

exec format error signifie que le noyau ne sait pas exécuter ce binaire : revenez à la plateforme exportée.

La voie par registre transporte un registre de la machine connectée vers la machine isolée : l'image du registre, et son volume, le dossier où il range les images poussées. Une fois relancé sur la machine isolée, il sert les images comme un registre ordinaire, et on les tire par digest.

Commencez par alimenter le registre de la machine connectée. Un docker push d'une image dont seule la plateforme locale est présente n'envoie que cette plateforme, et le registre reçoit alors le digest du manifeste amd64, pas celui de l'index. Poussez la copie du socle et l'application, et lisez les digests reçus :

Fenêtre de terminal
docker tag alpine:3.24.3 127.0.0.1:5001/reference/alpine:3.24.3
docker push 127.0.0.1:5001/reference/alpine:3.24.3 | tail -1
docker tag demo-web:1.0 127.0.0.1:5001/demo-web:1.0
docker push 127.0.0.1:5001/demo-web:1.0 | tail -1
curl -s http://127.0.0.1:5001/v2/_catalog
sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a -> sha256:c26612752c78b18faacd63c7d50f4592c2e7b0f25e0e20e82bd74e465c57ab0d
1.0: digest: sha256:afba17b4f22057ebb22ee737fb4ff0c6b3dfad0434ec348ea003bad9525fc9c5 size: 855
{"repositories":["demo-web","reference/alpine","socle-maison"]}

La première ligne dit que l'index be26… est devenu le manifeste c266… : le digest épinglé sur Docker Hub n'existe donc pas dans ce registre, qui ne connaît que le manifeste amd64. Un FROM épinglé sur l'index de Hub ne se résoudra pas sur cette copie. L'image construite localement, elle, garde son index (afba17b4…). Pour recopier un index complet vers un registre, skopeo copy --all copie l'index et toutes ses variantes, alors que sans elle seule la plateforme de la machine est copiée. La page Cache BuildKit en CI et promotion d'image entre registres mesure cette copie vers un registre ; le guide Skopeo mesure --all vers une archive.

Transportez ensuite le registre. Arrêtez-le d'abord, pour que son volume ne bouge pas pendant la copie. Archivez le volume avec un conteneur jetable, exportez l'image registry, et écrivez un reçu pour les deux fichiers.

Fenêtre de terminal
docker stop registre-livraison
umask 077
docker run --rm -v livraison-vol:/v:ro alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a tar cf - -C /v . > xfer/registre-data.tar
docker tag registry@sha256:d4032d4cc323c1731c707931ecada07a53076eac38e3b61ae57533f170f2c6c3 registry:3.1.2
docker save -o xfer/registry-image.tar registry:3.1.2
(cd xfer && sha256sum registre-data.tar registry-image.tar > transfert.sha256 && cat transfert.sha256)

Le volume pèse environ 4 Mo et l'image du registre environ 21 Mo. Après le docker stop, docker ps -a montre registre-livraison en Exited (2) : c'est le code de sortie du registre arrêté par le signal, pas un échec. Le docker tag répond au piège de pull tag@digest vu plus haut : l'image registry a été tirée par digest, donc sans tag.

La redirection > du shell crée registre-data.tar avec les droits de votre umask, souvent lisibles par les autres comptes : le umask 077 placé juste avant la commande réserve le fichier à votre compte, comme l'archive de docker save. Il reste actif dans ce terminal jusqu'à sa fermeture. Le reçu transfert.sha256 est rangé dans xfer pour la démonstration, mais il se transmet à part des archives, par un autre canal, sans quoi il ne protège pas d'une altération volontaire.

Sur la machine isolée, vérifiez les sommes, chargez l'image du registre, restaurez le volume, puis relancez le registre. Le conteneur qui restaure le volume emploie l'image alpine : elle doit déjà être chargée sur la machine isolée, ce qui est le cas après le docker load de alpine.tar plus haut, car aucun pull n'est possible.

Fenêtre de terminal
docker exec machine-isolee sh -c 'cd /xfer && sha256sum -c transfert.sha256'
docker exec machine-isolee docker load -i /xfer/registry-image.tar
docker exec machine-isolee docker volume create regvol
docker exec machine-isolee docker run --rm -v regvol:/v -v /xfer:/xfer:ro alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a tar xf /xfer/registre-data.tar -C /v
docker exec machine-isolee docker run -d --name registre-livraison -p 127.0.0.1:5000:5000 -v regvol:/var/lib/registry registry:3.1.2@sha256:d4032d4cc323c1731c707931ecada07a53076eac38e3b61ae57533f170f2c6c3
docker exec machine-isolee wget -qO- http://127.0.0.1:5000/v2/_catalog
docker exec machine-isolee wget -qO- http://127.0.0.1:5000/v2/demo-web/tags/list
registre-data.tar: OK
registry-image.tar: OK
Loaded image: registry:3.1.2
{"repositories":["demo-web","reference/alpine","socle-maison"]}
{"name":"demo-web","tags":["1.0"]}

Le catalogue est identique à celui de la machine connectée : le registre a bien repris son contenu. Reste à tirer l'image par son digest et à la lancer sans réseau. Le --pull never est ici la preuve : il interdit tout accès, et l'image doit déjà être là.

Fenêtre de terminal
docker exec machine-isolee docker pull 127.0.0.1:5000/demo-web@sha256:afba17b4f22057ebb22ee737fb4ff0c6b3dfad0434ec348ea003bad9525fc9c5 | tail -2
docker exec machine-isolee docker run --rm --network none --pull never 127.0.0.1:5000/demo-web@sha256:afba17b4f22057ebb22ee737fb4ff0c6b3dfad0434ec348ea003bad9525fc9c5
Status: Downloaded newer image for 127.0.0.1:5000/demo-web@sha256:afba17b4f22057ebb22ee737fb4ff0c6b3dfad0434ec348ea003bad9525fc9c5
127.0.0.1:5000/demo-web@sha256:afba17b4f22057ebb22ee737fb4ff0c6b3dfad0434ec348ea003bad9525fc9c5
version-1

La sortie version-1 vient du conteneur lancé sans réseau, depuis une image tirée d'un registre qui n'existait pas sur cette machine avant le chargement du volume. Deux erreurs se reconnaissent à leur message. Un digest que le registre ne possède pas, ou un dépôt qui n'existe pas, répondent not found :

Error response from daemon: failed to resolve reference "127.0.0.1:5000/demo-web@sha256:0000000000000000000000000000000000000000000000000000000000000000": 127.0.0.1:5000/demo-web@sha256:0000000000000000000000000000000000000000000000000000000000000000: not found
Error response from daemon: failed to resolve reference "127.0.0.1:5000/inconnu:1.0": 127.0.0.1:5000/inconnu:1.0: not found

Ce registre reste en HTTP sur la boucle locale. Servi à d'autres machines, il doit passer en TLS avec authentification, comme dans la leçon sur le registre de confiance.

Pourquoi un miroir pull-through n'est-il pas une solution hors ligne ?

Section intitulée « Pourquoi un miroir pull-through n'est-il pas une solution hors ligne ? »

Un miroir pull-through est un registre qui relaie un registre amont et garde une copie de ce qu'on lui demande. Le démarrer avec REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io donne un miroir de Docker Hub. En ligne, il fonctionne : un docker pull d'une image par digest réussit, et son cache grossit. Mais hors ligne il ne reste pas démarré : avec registry 3.1.2, il contacte l'amont au lancement et s'arrête en panique.

Fenêtre de terminal
docker exec machine-isolee docker volume create miroir
docker exec machine-isolee docker run -d --name miroir -p 127.0.0.1:5001:5000 -v miroir:/var/lib/registry -e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io registry:3.1.2@sha256:d4032d4cc323c1731c707931ecada07a53076eac38e3b61ae57533f170f2c6c3
sleep 15
docker exec machine-isolee docker logs miroir 2>&1 | grep '^panic'
docker exec machine-isolee docker pull 127.0.0.1:5001/library/busybox:1.37
panic: Get "https://registry-1.docker.io/v2/": dial tcp: lookup registry-1.docker.io on 10.10.114.1:53: read udp 172.17.0.4:45198->10.10.114.1:53: i/o timeout
Error response from daemon: failed to resolve reference "127.0.0.1:5001/library/busybox:1.37": failed to do request: Head "https://127.0.0.1:5001/v2/library/busybox/manifests/1.37": dial tcp 127.0.0.1:5001: connect: connection refused

La première ligne est le message de panique du miroir : il contacte l'amont à son démarrage, n'obtient aucune réponse (délai dépassé sur la résolution du nom), panique et s'arrête. Quelques secondes après le lancement, le conteneur passe de Up à Exited (2).

Le pull échoue donc de deux façons. Tant que le conteneur est Up, il répond connection reset by peer ; une fois arrêté, il répond connection refused, puisque plus rien n'écoute sur le port. C'est cette seconde erreur que montre la sortie ci-dessus, relevée après le sleep 15. Un miroir pull-through accélère les pulls et réduit les appels vers Hub tant que le réseau fonctionne, mais il ne remplace pas une livraison hors ligne.

Son cache, en revanche, est un simple volume de registre. Les étapes ci-dessous se reproduisent en exercice, sur le modèle de la section précédente (la sortie montrée plus bas vient de ce déroulé) :

  1. démarrez le même miroir côté connecté, dans un conteneur miroir-connecte publié avec -p 127.0.0.1:5003:5000, dont le volume miroir-cache est monté sur /var/lib/registry ;
  2. tirez-y l'image par son digest, avec docker pull 127.0.0.1:5003/library/hello-world@sha256:c567b231f65614688711a75f31518a064258a51d442117a3e771a3a8fdba78ef : le miroir la télécharge depuis Docker Hub et la garde dans son volume ;
  3. arrêtez-le, puis archivez son volume comme celui de la section précédente ;
  4. restaurez l'archive dans le volume miroir de la machine isolée ;
  5. retirez côté connecté le conteneur miroir-connecte, son volume miroir-cache et l'image tirée à l'étape 2, que le nettoyage final traite aussi.

Servi par un registre ordinaire, sans la variable REGISTRY_PROXY_REMOTEURL, ce volume répond :

Fenêtre de terminal
docker exec machine-isolee docker rm -f miroir
docker exec machine-isolee docker run -d --name miroir-nu -p 127.0.0.1:5002:5000 -v miroir:/var/lib/registry registry:3.1.2@sha256:d4032d4cc323c1731c707931ecada07a53076eac38e3b61ae57533f170f2c6c3
docker exec machine-isolee docker pull 127.0.0.1:5002/library/hello-world@sha256:c567b231f65614688711a75f31518a064258a51d442117a3e771a3a8fdba78ef
docker exec machine-isolee docker run --rm --pull never --network none 127.0.0.1:5002/library/hello-world@sha256:c567b231f65614688711a75f31518a064258a51d442117a3e771a3a8fdba78ef
Status: Downloaded newer image for 127.0.0.1:5002/library/hello-world@sha256:c567b231f65614688711a75f31518a064258a51d442117a3e771a3a8fdba78ef
127.0.0.1:5002/library/hello-world@sha256:c567b231f65614688711a75f31518a064258a51d442117a3e771a3a8fdba78ef
Hello from Docker!

Le cache d'un miroir se transporte donc comme n'importe quel volume de registre. Il ne contient que les images déjà demandées à ce miroir : ce qu'on n'a jamais tiré en ligne n'y est pas. La documentation de Distribution précise aussi que le miroir supprime périodiquement le vieux contenu de son cache : un volume copié longtemps après le dernier pull peut avoir perdu des images.

Chaque échec de la leçon a un message qui le désigne. Cherchez le vôtre dans la première colonne.

Message ou symptômeCauseGeste
failed to resolve source metadata … not found dans un build, sur une machine qui joint le registreLe registre répond, mais il ne possède pas le digest du FROMRelever le digest d'index avec docker buildx imagetools inspect --format '{{.Manifest.Digest}}'
La ligne RUN cat /etc/alpine-release affiche une autre version que le tag du FROMLe tag est ignoré quand un digest est présentCorriger le tag pour qu'il corresponde au digest épinglé
docker save busybox:1.37 : No such image après un pull par digestpull tag@digest ne pose pas le tagdocker tag busybox@sha256:… busybox:1.37, puis docker save
no suitable export target found … does not provide the specified platformLa plateforme demandée à --platform n'est pas dans le magasin localLa tirer d'abord avec docker pull --platform
failed to ingest … short read … unexpected EOF au docker loadArchive tronquée pendant le transfertRefaire le transfert, puis sha256sum -c
Error unpacking image … content digest … not found avec rc=0Archive altérée : docker load ne le signale pas par son codeVérifier le reçu sha256sum -c avant tout chargement
No such image: alpine@sha256:<digest d'index> après le chargement d'une archive faite avec --platformL'archive ne porte que le manifeste : l'image chargée a le digest du manifeste, pas celui de l'indexRefaire l'archive sans --platform, ou épingler le FROM sur le digest du manifeste
failed to do request: Head "…/manifests/sha256:<digest d'index>" … network is unreachable au build d'un FROM épinglé sur l'index, après le chargement d'une archive faite avec --platformL'image chargée porte le digest du manifeste : le moteur ne trouve pas l'index en local et le cherche sur le registre, injoignableRefaire l'archive sans --platform, ou épingler le FROM sur le digest du manifeste
exec format error au lancement d'une image livréeArchive d'une autre architecture que la machineExporter la bonne plateforme (tirer cette variante, puis --platform)
network is unreachable au docker pull (pour un build, voir la ligne sur le digest d'index)La machine n'a pas de réseau, c'est attenduLivrer par archive ou registre, et lancer avec --pull never
No such image avec --pull neverL'image n'a pas été chargéedocker load, ou docker pull depuis le registre livré
panic: Get "https://registry-1.docker.io/v2/" dans les journaux d'un registre, puis connection reset by peer ou connection refused au docker pullUn miroir pull-through contacte l'amont au démarrage, panique et s'arrête (Exited (2))Servir le volume de cache par un registre ordinaire
You have reached your pull rate limit (HTTP 429) au docker pull ou au script verifie-socle.shDocker Hub limite les pulls d'une adresse ou d'un compteS'authentifier ou attendre, voir Publier et récupérer une image
failed to resolve reference … : not found sur le registre livréDigest ou dépôt absent du registre transportéLire _catalog et tags/list pour voir ce qu'il contient

Un échec à l'étape du docker pull par digest, après une livraison par registre, peut venir d'un digest copié depuis Docker Hub : rappelez-vous que docker push d'une image locale n'envoie que le manifeste de la plateforme locale, dont le digest diffère de celui de l'index.

La leçon laisse sur votre poste un conteneur privilégié, un registre, un volume, des images et des archives. Retirez d'abord la machine isolée : ses volumes regvol et miroir, ses conteneurs registre-livraison et miroir-nu et les images qu'elle a chargées vivent tous dans le démon qu'elle contient. L'option -v de docker rm -fv compte ici : l'image docker:dind déclare /var/lib/docker comme volume, et Docker en crée un volume anonyme qui porte tout le démon interne et les données de la livraison. Sans -v, le volume anonyme du démon reste sur votre poste.

Retirez ensuite le registre de la machine connectée et son volume livraison-vol, puis les images de la leçon et les dossiers de transfert. Si une commande docker image rm répond qu'une image n'existe pas, c'est qu'elle a déjà disparu : les autres noms de la liste sont quand même traités, mais la commande sort en code 1. Si elle répond qu'une image est utilisée par un conteneur, supprimez d'abord ce conteneur avec docker rm -f.

Fenêtre de terminal
docker rm -fv machine-isolee
docker rm -f registre-livraison
docker volume rm livraison-vol
# seulement si vous avez fait l'exercice du miroir côté connecté
docker rm -f miroir-connecte
docker volume rm miroir-cache
docker image rm 127.0.0.1:5003/library/hello-world@sha256:c567b231f65614688711a75f31518a064258a51d442117a3e771a3a8fdba78ef
docker image rm t:menteur demo-web:1.0 127.0.0.1:5001/demo-web:1.0 127.0.0.1:5001/reference/alpine:3.24.3
docker image rm busybox:1.37 registry:3.1.2 alpine:3.24.3
docker image rm docker:dind@sha256:9e79b879b177fbd72c34376a3dd3d91747da5db49e1f560f76aea59561471977
rm -rf xfer alt v1 v2 app menteur verifie-socle.sh

Les images alpine, busybox et registry servent peut-être à d'autres usages sur votre machine : gardez-les si c'est le cas. Vérifiez avec docker image ls qu'il ne reste aucune image <none> issue de la leçon : une image tirée par digest sans tag, ou le socle version-1 devenu orphelin quand vous l'avez reconstruit sous le même tag. Supprimez-la alors par son identifiant. L'image 127.0.0.1:5001/socle-maison:2026-11 n'apparaît pas dans la liste : la section sur le tag déplacé l'a déjà supprimée.

Le dernier rm -rf retire aussi les dossiers v1/, v2/, app/ et menteur/ et le script verifie-socle.sh, écrits au fil des sections. L'option -f évite une erreur pour un dossier que vous n'avez pas créé, alt/ par exemple si vous avez sauté la démonstration d'altération. Relisez la liste avant de lancer la commande : elle ne demande aucune confirmation.

Le premier lab vous demande de garder un build sur le socle prévu alors que le tag de ce socle change dans un registre local : la validation reconstruit l'image après un changement de source et contrôle le socle effectivement livré. Le second vous demande de préparer une image publiée pour une machine sans réseau, avec sa somme d'intégrité : la validation contrôle que l'image se récupère hors ligne sans dépendre du cache Docker local.

Le digest d'index d'un tag se lit avec docker buildx imagetools inspect <tag> --format '{{.Manifest.Digest}}', sans tirer l'image, et s'écrit dans le FROM sous la forme nom:tag@sha256:…. Quand un digest est présent, Docker ignore le tag sans avertir : un tag qui ne correspond pas au digest trompe le lecteur, pas le build.

Un build épinglé reste sur l'ancien contenu quand le tag est déplacé, tant que le registre sert encore ce digest. Pour savoir qu'un socle plus récent existe, comparez le digest épinglé à celui que le registre sert pour le tag, ce qui suppose de garder le tag dans le FROM, et faites échouer la tâche par un code de retour non nul.

docker pull tag@digest ne pose pas le tag : sans docker tag, un docker save par nom échoue. docker save sans option garde l'index et exporte les plateformes présentes dans le magasin local. --platform ne choisit que parmi elles et réduit l'archive à un manifeste : l'image chargée porte alors le digest du manifeste, et un FROM épinglé sur l'index ne s'y résout plus. Pour garder le digest d'index, exportez sans --platform. L'archive est créée en -rw-------.

Le code de retour de docker load ne prouve pas l'intégrité : une archive altérée se charge avec le code 0. On écrit un reçu sha256sum avant le transfert et on le vérifie avec sha256sum -c avant le chargement. Ce reçu protège d'une archive abîmée ; contre une altération volontaire, il doit voyager par un autre canal que l'archive.

Livrer par registre revient à transporter l'image registry et son volume, arrêté pendant la copie, puis à tirer l'image par digest sur la machine isolée. Un docker push d'une image locale n'envoie que la plateforme locale, dont le digest n'est pas celui de l'index. --pull never prouve que l'exécution n'a pas besoin du réseau.

Un miroir pull-through ne remplace pas une livraison hors ligne : il s'arrête en panique peu après son lancement quand son amont est injoignable. Son cache, servi par un registre ordinaire, répond, mais il ne contient que ce qui avait été demandé en ligne. Le conteneur --privileged --network none qui simule la machine isolée est un moyen de démonstration, pas une méthode de livraison.

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

8 questions
8 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