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

Construire une image pour plusieurs architectures

Par mis à jour le

Améliorer cette page

Logo Docker

Une image multi-architecture est un index : un tag unique derrière lequel Docker choisit la variante qui correspond à la machine (amd64 sur un PC, arm64 sur un serveur ARM). Cette leçon construit un tel index avec Docker Buildx, le publie dans un registre local, puis exécute chaque variante pour vérifier que le programme qu'elle contient est bien celui de sa plateforme.

Un build qui réussit ne prouve pas que l'image fonctionne : une variante arm64 peut très bien contenir un binaire amd64, et seul un lancement le montre. Vous apprendrez aussi à reconnaître l'erreur exec format error, à choisir entre émulation et compilation croisée, et à déclarer ce que vous n'avez pas rejoué. Toutes les mesures viennent de Docker Engine 29.9.2, Buildx 0.38.1 et BuildKit v0.34.0, avec le magasin d'images containerd, sur une VM Debian 13 amd64.

Le cache distribué entre exécutions de CI est traité dans Cache BuildKit en CI et promotion d'image entre registres. Cette leçon enregistre l'émulateur QEMU à la main, sur la VM de mesure ; son enregistrement sur un runner de CI n'a pas été mesuré.

  • Lire les plateformes d'un builder et créer un builder docker-container, puis le retirer
  • Reconnaître exec format error et enregistrer l'émulation QEMU
  • Construire deux plateformes en émulation puis en compilation croisée, et comparer les durées
  • Repérer le piège d'un binaire de la mauvaise architecture dans un index pourtant correct
  • Lire un index publié sans compter ses entrées, et exécuter chaque variante par son digest
  • Prévoir ce que fait docker run --platform selon ce qui est déjà en local
  • Déclarer ce qui n'a pas été rejoué sur du matériel réel

Quel builder et quelles plateformes avez-vous sans rien configurer ?

Section intitulée « Quel builder et quelles plateformes avez-vous sans rien configurer ? »

Un builder est le composant qui exécute vos constructions : il fait tourner BuildKit, le moteur de construction de Docker, et son pilote (driver) dit où BuildKit s'exécute. Le builder utilisé par défaut s'appelle default et fonctionne avec le pilote docker : BuildKit est alors intégré au moteur.

Vérifiez d'abord le magasin d'images, c'est-à-dire l'endroit où Docker range les images et leurs couches, car la construction multi-plateforme en dépend.

Fenêtre de terminal
docker info | grep -Ei "storage|snapshotter|Server Version|Architecture"
Server Version: 29.9.2
Storage Driver: overlayfs
driver-type: io.containerd.snapshotter.v1
Architecture: x86_64

La ligne driver-type: io.containerd.snapshotter.v1 indique le magasin containerd, celui que Docker choisit seul sur une nouvelle installation de Docker Engine 29 ou plus (une mise à jour garde l'ancien magasin ; docker info dit lequel vous utilisez). C'est lui qui sait garder, côte à côte, plusieurs plateformes sous un même tag. Les mesures de cette leçon ne valent pas pour l'ancien magasin classique, que la leçon n'a pas testé. Listez maintenant les builders.

Fenêtre de terminal
docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default* docker
\_ default \_ default running v0.34.0 linux/amd64 (+3)

L'étoile marque le builder actif. La colonne PLATFORMS est abrégée : (+3) signale trois plateformes de plus. La commande docker buildx inspect --bootstrap donne la liste complète ; la sortie ci-dessous est réduite à ses premières lignes.

Fenêtre de terminal
docker buildx inspect --bootstrap
Name: default
Driver: docker
Nodes:
Name: default
Endpoint: default
Status: running
BuildKit version: v0.34.0
Platforms: linux/amd64, linux/amd64/v2, linux/amd64/v3

Sans rien configurer, le builder ne connaît que les variantes de votre processeur. Pour exécuter des instructions arm64 pendant une construction, il manque un émulateur, que la section sur l'émulation ajoute.

Que change un builder docker-container, et comment le retirer ?

Section intitulée « Que change un builder docker-container, et comment le retirer ? »

Un builder docker-container lance BuildKit dans un conteneur à part, au lieu d'utiliser celui de Docker : vous le créez pour disposer d'un BuildKit distinct. Créez un builder nommé multi avec une image BuildKit épinglée par version et par digest, et démarrez-le.

Fenêtre de terminal
docker buildx create --name multi --driver docker-container \
--driver-opt image=moby/buildkit:v0.34.0@sha256:b059f8d7226d0b326bc871af489a5dddf65e36af4d20d14251b072974d9ee87c --use
docker buildx inspect --bootstrap multi
multi
#1 [internal] booting buildkit
#1 pulling image moby/buildkit:v0.34.0@sha256:b059f8d7226d0b326bc871af489a5dddf65e36af4d20d14251b072974d9ee87c
#1 pulling image moby/buildkit:v0.34.0@sha256:b059f8d7226d0b326bc871af489a5dddf65e36af4d20d14251b072974d9ee87c 9.1s done
#1 creating container buildx_buildkit_multi0
#1 creating container buildx_buildkit_multi0 0.9s done
#1 DONE 10.0s
Name: multi
Driver: docker-container
[...]
Nodes:
Name: multi0
Endpoint: unix:///var/run/docker.sock
Driver Options: image="moby/buildkit:v0.34.0@sha256:b059f8d7226d0b326bc871af489a5dddf65e36af4d20d14251b072974d9ee87c"
Status: running
BuildKit daemon flags: --allow-insecure-entitlement=network.host
BuildKit version: v0.34.0
Platforms: linux/amd64, linux/amd64/v2, linux/amd64/v3, linux/386
[...]

Lisez trois choses dans la sortie de buildx inspect. Docker tire l'image BuildKit épinglée, puis crée le conteneur buildx_buildkit_multi0. Avec cette image, la ligne BuildKit version doit afficher v0.34.0, la même version que le builder par défaut. Sans l'option --driver-opt image=…, Buildx tire moby/buildkit:buildx-stable-1, un tag mobile : lors de la mesure de la leçon, il donnait BuildKit v0.33.1, quand le builder par défaut exécutait v0.34.0. Enfin, la ligne Platforms liste ce que le builder gère : avec l'image épinglée, ce sont les plateformes de votre processeur et linux/386, sans arm64.

La documentation Docker sur le multi-plateforme indique que les images officielles de BuildKit embarquent des émulateurs QEMU en mode utilisateur. La leçon n'a pas vérifié que ces émulateurs servent : la liste Platforms ci-dessus ne contenait pas arm64, et c'est elle qui fait foi. Toutes les mesures d'émulation de la leçon portent sur le builder par défaut de Docker, avec l'émulation enregistrée par binfmt ; seule la publication vers un registre local, plus bas, passe par un builder docker-container.

Un builder docker-container garde ses résultats dans son propre conteneur. Pour retrouver une image dans docker image ls, il faut la demander avec --load ; --push l'envoie au registre sans rien charger en local. Une autre différence, le réseau, apparaît quand on publie vers un registre local ; la section sur l'index publié la traite. Revenez au builder par défaut, retirez multi, puis vérifiez qu'il ne reste ni builder ni conteneur.

Fenêtre de terminal
docker buildx use default
docker buildx rm multi
docker buildx ls
docker ps -a --format "{{.Names}} {{.Image}}"
multi removed
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default* docker
\_ default \_ default running v0.34.0 linux/amd64 (+3)

Le message multi removed confirme le retrait, et docker ps -a n'affiche aucune ligne buildx_buildkit_multi0 : le conteneur du builder a disparu avec lui. L'image BuildKit reste en revanche dans docker image ls ; la section de nettoyage la supprime.

Pourquoi un programme arm64 échoue-t-il avec exec format error ?

Section intitulée « Pourquoi un programme arm64 échoue-t-il avec exec format error ? »

Un processeur amd64 ne sait pas exécuter un programme compilé pour arm64. Docker peut tirer l'image arm64 sans difficulté, car télécharger des fichiers n'exige aucune exécution. Le test se fait avec Alpine 3.24.3, épinglée par digest.

Fenêtre de terminal
docker pull --platform linux/arm64 alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
docker run --rm --platform linux/arm64 alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a uname -m
echo "code=$?"
docker.io/library/alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a: Pulling from library/alpine
d3531b7a4f21: Pulling fs layer
d3531b7a4f21: Download complete
2a02c517eb64: Download complete
4f276f238bfa: Download complete
d3531b7a4f21: Pull complete
Digest: sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
Status: Image is up to date for alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
docker.io/library/alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
exec /bin/uname: exec format error
code=255

Le tirage réussit (sur la machine de mesure, l'index d'Alpine était déjà présent, d'où la ligne Image is up to date : elle ne signale pas une absence de variante arm64). Le lancement échoue avec exec format error et le code 255 : le noyau lit un fichier d'une autre architecture et ne sait pas l'exécuter. La même erreur apparaît dans une construction dès qu'une instruction RUN doit exécuter un programme de la plateforme cible. Voici le Dockerfile de test, qui ne contient qu'un RUN.

Fenêtre de terminal
mkdir -p ~/multiarch && cd ~/multiarch
Dockerfile.run
FROM alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
RUN uname -m > /arch
CMD ["cat", "/arch"]
Fenêtre de terminal
docker buildx build --platform linux/arm64 -f Dockerfile.run -t demo/plateformes:run-arm64 --load .
echo "build-exit=$?"
#5 [2/2] RUN uname -m > /arch
#5 0.186 exec /bin/sh: exec format error
#5 ERROR: process "/bin/sh -c uname -m > /arch" did not complete successfully: exit code: 255
[...]
ERROR: failed to build: failed to solve: process "/bin/sh -c uname -m > /arch" did not complete successfully: exit code: 255
build-exit=1

La sortie est abrégée. L'instruction FROM de l'étape précédente s'est terminée sans erreur : charger et extraire les couches d'une image arm64 n'exécute rien. C'est le RUN, qui lance /bin/sh de l'image, qui échoue. Une instruction COPY, qui recopie seulement des fichiers, n'exécute aucun programme non plus. Retenez la règle : seuls les RUN exigent l'émulation.

Comment enregistrer l'émulation QEMU, et comment la retirer ?

Section intitulée « Comment enregistrer l'émulation QEMU, et comment la retirer ? »

L'émulation consiste à faire exécuter un programme arm64 par QEMU, un logiciel qui imite ce processeur. Elle passe par binfmt_misc, un mécanisme du noyau Linux qui associe un format de fichier à l'interpréteur chargé de le lancer. Si QEMU est enregistré pour arm64, le noyau lui confie les binaires arm64 et le conteneur s'exécute. Regardez d'abord ce qui est enregistré sur la machine.

Fenêtre de terminal
ls /proc/sys/fs/binfmt_misc
python3.13
register
status

Il n'y a aucune entrée qemu-* : seul l'interpréteur Python est déclaré. Notez la liste que vous obtenez chez vous : le nettoyage en fin de leçon s'y réfère pour ne pas retirer une émulation qui existait déjà.

L'image tonistiigi/binfmt enregistre les émulateurs. Son tag latest se déplace à chaque publication : la leçon épingle donc le tag de version qemu-v10.2.3-68 et son digest. Lancée sans option, elle ne modifie rien et liste ce que le noyau accepte.

Fenêtre de terminal
docker run --privileged --rm tonistiigi/binfmt:qemu-v10.2.3-68@sha256:400a4873b838d1b89194d982c45e5fb3cda4593fbfd7e08a02e76b03b21166f0
{
"supported": [
"linux/amd64",
"linux/amd64/v2",
"linux/amd64/v3",
"linux/386"
],
"emulators": [
"python3.13"
]
}

Au premier lancement, Docker tire d'abord l'image : des lignes Pulling from tonistiigi/binfmt précèdent le JSON, non reproduites ici. L'option --privileged est nécessaire parce que ce conteneur écrit dans la configuration du noyau de l'hôte, partagée par tous les conteneurs. C'est aussi pourquoi l'image est épinglée par digest : un conteneur privilégié ne doit pas pouvoir changer à votre insu. Enregistrez maintenant l'émulateur arm64.

Fenêtre de terminal
docker run --privileged --rm tonistiigi/binfmt:qemu-v10.2.3-68@sha256:400a4873b838d1b89194d982c45e5fb3cda4593fbfd7e08a02e76b03b21166f0 --install arm64
installing: arm64 OK
{
"supported": [
"linux/amd64",
"linux/amd64/v2",
"linux/amd64/v3",
"linux/arm64",
"linux/386"
],
"emulators": [
"python3.13",
"qemu-aarch64"
]
}

La plateforme linux/arm64 apparaît dans supported et qemu-aarch64 dans emulators. Le noyau possède désormais une entrée que vous pouvez lire.

Fenêtre de terminal
cat /proc/sys/fs/binfmt_misc/qemu-aarch64
enabled
interpreter /usr/bin/qemu-aarch64
flags: POCF
offset 0
magic 7f454c460201010000000000000000000200b700
mask ffffffffffffff00fffffffffffffffffeffffff

Le champ magic est la signature d'un exécutable arm64 au format ELF (le format des exécutables Linux), et interpreter désigne l'émulateur chargé de le lancer. Relancez la commande qui échouait, puis la construction.

Fenêtre de terminal
docker run --rm --platform linux/arm64 alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a uname -m
cd ~/multiarch && docker buildx build --platform linux/arm64 -f Dockerfile.run -t demo/plateformes:run-arm64 --load .
docker run --rm demo/plateformes:run-arm64
aarch64
WARNING: The requested image's platform (linux/arm64) does not match the detected host platform (linux/amd64/v3) and no specific platform was requested
aarch64

La première ligne, aarch64, est la réponse du conteneur arm64 (aarch64 est le nom que uname donne à cette architecture) : l'erreur 255 a disparu. La construction affiche sa progression, non reproduite ici, et dure moins d'une seconde sur la machine de mesure ; docker run répond ensuite aarch64, précédé d'un avertissement.

Cet avertissement ne signale pas une panne : le tag désigne une image arm64 sur une machine amd64, et Docker le dit parce que vous n'avez pas précisé --platform.

Pour retirer l'émulation, passez --uninstall avec le nom de l'entrée du noyau.

Fenêtre de terminal
docker run --privileged --rm tonistiigi/binfmt:qemu-v10.2.3-68@sha256:400a4873b838d1b89194d982c45e5fb3cda4593fbfd7e08a02e76b03b21166f0 --uninstall qemu-aarch64
uninstalling: qemu-aarch64 OK
{
"supported": [
"linux/amd64",
"linux/amd64/v2",
"linux/amd64/v3",
"linux/386"
],
"emulators": [
"python3.13"
]
}

La liste revient à l'état initial. Réenregistrez l'émulation avec la même commande --install arm64, et gardez-la jusqu'à la section de nettoyage, qui la retire.

Fenêtre de terminal
docker run --privileged --rm tonistiigi/binfmt:qemu-v10.2.3-68@sha256:400a4873b838d1b89194d982c45e5fb3cda4593fbfd7e08a02e76b03b21166f0 --install arm64

La sortie est celle de la première installation : installing: arm64 OK, puis la liste où figurent linux/arm64 et qemu-aarch64.

Comment construire une image pour deux plateformes ?

Section intitulée « Comment construire une image pour deux plateformes ? »

Enregistrez chaque fichier de cette leçon dans ~/multiarch, sous le nom indiqué en titre du bloc. L'option --platform accepte une liste, et --load rapatrie le résultat dans le magasin local. Avec le magasin containerd, ces deux options fonctionnent ensemble avec le builder par défaut, sans builder supplémentaire. Le programme d'essai est un petit binaire Go qui affiche l'architecture pour laquelle il a été compilé, et un mot écrit à l'envers pour qu'il soit reconnaissable.

main.go
package main
import (
"fmt"
"runtime"
)
func reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
func main() {
fmt.Printf("GOOS=%s GOARCH=%s app=%s\n", runtime.GOOS, runtime.GOARCH, reverse("plateformes"))
}
go.mod
module demo/plateformes
go 1.27

Le premier Dockerfile compile dans l'image golang de la plateforme cible : sous arm64, le compilateur lui-même est un programme arm64, donc émulé. L'image Go est épinglée par le digest de son index.

Dockerfile.emul
FROM golang:1.27-alpine@sha256:d200d2dd76b03a99b52651fd1325aff7dcd35422c35c01bac56571b0507f3deb AS build
WORKDIR /src
COPY go.mod main.go ./
RUN CGO_ENABLED=0 go build -o /out/app .
FROM alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]

Tirez d'abord l'image Go pour les deux plateformes, afin que le téléchargement ne compte pas dans la durée. L'option --no-cache force ensuite la compilation à s'exécuter : sans elle, un cache déjà rempli fausserait la mesure.

Fenêtre de terminal
docker pull --platform linux/amd64 golang:1.27-alpine@sha256:d200d2dd76b03a99b52651fd1325aff7dcd35422c35c01bac56571b0507f3deb | tail -2
docker pull --platform linux/arm64 golang:1.27-alpine@sha256:d200d2dd76b03a99b52651fd1325aff7dcd35422c35c01bac56571b0507f3deb | tail -2
time docker buildx build --no-cache --platform linux/amd64,linux/arm64 -f Dockerfile.emul -t demo/plateformes:emul --load --progress=plain . 2>&1 | tail -25
[...]
#18 [linux/amd64 stage-1 2/2] COPY --from=build /out/app /app
#18 DONE 0.1s
#17 [linux/arm64 build 4/4] RUN CGO_ENABLED=0 go build -o /out/app .
#17 DONE 51.2s
#19 [linux/arm64 stage-1 2/2] COPY --from=build /out/app /app
#19 DONE 0.1s
#20 exporting to image
#20 exporting layers
#20 exporting layers 0.3s done
#20 exporting manifest sha256:47eabc785aa6062c7586bf0d0f1ce418872bf936d76cd9845dccb68c86a87b2c
#20 exporting manifest sha256:47eabc785aa6062c7586bf0d0f1ce418872bf936d76cd9845dccb68c86a87b2c 0.0s done
#20 exporting config sha256:48f1a928b8e92d4358ac4f7cb83b06848580009f8fef7f0a3c83f0221e9a80b6 0.0s done
#20 exporting attestation manifest sha256:b08516758cca3cb0805adf6cc8efec81d790cdad081267fd98aca3f1f7e7e478 0.0s done
#20 exporting manifest sha256:ff6c1d79dadae09bd2037f3bb02364d335125f93b84587c6eb63251f1049b269 0.0s done
#20 exporting config sha256:06478064ef7e28675e51f48ddb7ab6e6b9e9c4dc3f347533d83dcc41db4bcac4 0.0s done
#20 exporting attestation manifest sha256:f2fd51c8dcefeeb8713fb17b2670b1f7891dc8c001d5270cb29d97928f7d1b10 0.0s done
#20 exporting manifest list sha256:870f4914b947c2e5656e723ab9eb4d416ff2fc2e30c695c0b07220bd981cccc4
#20 exporting manifest list sha256:870f4914b947c2e5656e723ab9eb4d416ff2fc2e30c695c0b07220bd981cccc4 0.0s done
#20 naming to docker.io/demo/plateformes:emul done
#20 unpacking to docker.io/demo/plateformes:emul 0.0s done
#20 DONE 0.6s
real 0m56.805s
user 0m0.097s
sys 0m0.027s

Cherchez deux repères. L'étape [linux/arm64 build 4/4], la compilation sous QEMU, dure 51,2 s ; la construction entière dure 56,8 s (ligne real).

Les lignes exporting montrent ce que produit une seule construction. Un manifeste est la fiche d'une variante : il liste ses couches et sa configuration. La construction en écrit un par plateforme, puis un index, que Docker appelle aussi liste de manifestes (manifest list), qui les réunit sous un seul tag. Les lignes attestation manifest sont expliquées avec l'index publié, dans la section qui lui est consacrée. Lisez l'index avec --tree.

Fenêtre de terminal
docker image ls --tree
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
demo/plateformes:emul 870f4914b947 22.3MB 10.8MB
├─ linux/amd64 47eabc785aa6 16.8MB 5.3MB
└─ linux/arm64 ff6c1d79dada 5.54MB 5.54MB

L'option --tree est marquée expérimentale dans le client Docker 29.9.2 : son affichage peut changer. La sortie est limitée à l'image de ce test : la liste complète contient aussi les images de base. La ligne du dessus est l'index, les deux lignes du dessous sont les variantes. Le champ ID d'une variante est l'identifiant de son manifeste. Pour examiner l'une d'elles, ajoutez --platform à docker image inspect, puis lancez-la avec la même option.

Fenêtre de terminal
docker image inspect --platform linux/arm64 --format '{{.Os}}/{{.Architecture}} id={{.Id}}' demo/plateformes:emul
docker run --rm --platform linux/arm64 demo/plateformes:emul
docker image inspect --platform linux/amd64 --format '{{.Os}}/{{.Architecture}} id={{.Id}}' demo/plateformes:emul
docker run --rm --platform linux/amd64 demo/plateformes:emul
linux/arm64 id=sha256:ff6c1d79dadae09bd2037f3bb02364d335125f93b84587c6eb63251f1049b269
GOOS=linux GOARCH=arm64 app=semrofetalp
linux/amd64 id=sha256:47eabc785aa6062c7586bf0d0f1ce418872bf936d76cd9845dccb68c86a87b2c
GOOS=linux GOARCH=amd64 app=semrofetalp

Chaque variante répond avec son architecture : GOARCH=arm64 pour l'une, GOARCH=amd64 pour l'autre. C'est le test à retenir, car le programme rapporte ce pour quoi il a été compilé, pas ce que docker image inspect déclare dans le manifeste.

Compiler en émulation fonctionne avec n'importe quel Dockerfile, mais le compilateur tourne lentement sous QEMU. La compilation croisée évite cette lenteur : le compilateur tourne en natif sur votre machine, et il produit un binaire pour une autre architecture. Go le permet avec deux variables, GOOS et GOARCH. Cette section compare les deux méthodes, puis montre comment la seconde se trompe.

Le schéma met les deux chemins en regard. Dans les deux, l'image finale ne fait qu'un COPY, et seul change l'endroit où tourne le compilateur. L'encadré mis en évidence est l'étape qui passe par QEMU.

Deux chaînes de construction de l'image arm64. En compilation émulée, l'image Go arm64 lance go build sous QEMU, ce qui produit un binaire arm64 copié dans l'image finale. En compilation croisée, l'image Go de la machine, amd64, lance go build en natif avec GOARCH=arm64, ce qui produit un binaire arm64 copié dans l'image finale.Compilation croiséeImage Go amd64celle de la machineRUN GOARCH=arm64 go buildle compilateur amd64tourne en natifBinaire arm64COPY dansl'image finaleCompilation émuléeImage Go arm64RUN go buildle compilateur arm64tourne sous QEMUBinaire arm64COPY dansl'image finale
Seul le compilateur change de place : sous QEMU d'un côté, en natif de l'autre. Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

BuildKit fournit à chaque plateforme des variables de construction. BUILDPLATFORM est la plateforme de la machine qui construit, TARGETOS et TARGETARCH celles de l'image visée. Elles ne sont disponibles dans une étape qu'une fois déclarées par ARG. L'instruction FROM --platform=$BUILDPLATFORM demande l'image du compilateur pour la machine qui construit, et non pour la cible.

Dockerfile
FROM --platform=$BUILDPLATFORM golang:1.27-alpine@sha256:d200d2dd76b03a99b52651fd1325aff7dcd35422c35c01bac56571b0507f3deb AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src
COPY go.mod main.go ./
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/app .
FROM alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]

L'étape finale ne contient qu'un COPY : elle n'exécute aucun programme, donc elle n'a pas besoin d'émulation, même pour arm64. Mesurez la durée dans les mêmes conditions, avec --no-cache.

Fenêtre de terminal
time docker buildx build --no-cache --platform linux/amd64,linux/arm64 -f Dockerfile -t demo/plateformes:cross --load --progress=plain . 2>&1 | tail -28
[...]
#12 [linux/amd64->arm64 build 4/4] RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o /out/app .
#12 DONE 7.4s
#13 [linux/amd64 build 4/4] RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/app .
#13 DONE 7.4s
#14 [linux/arm64 stage-1 2/2] COPY --from=build /out/app /app
#14 DONE 0.1s
#15 [linux/amd64 stage-1 2/2] COPY --from=build /out/app /app
#15 DONE 0.1s
#16 exporting to image
#16 exporting layers
#16 exporting layers 0.3s done
#16 exporting manifest sha256:25ae59cb20b1fb92a8e54f6581127b2a477814c6ecb923e1dec3bb53a10c46e6
#16 exporting manifest sha256:25ae59cb20b1fb92a8e54f6581127b2a477814c6ecb923e1dec3bb53a10c46e6 0.0s done
#16 exporting config sha256:2214fa5559caa4831b35d8c492b0e5b4ecda73153c84278b5b3c9d85d6a85439 0.0s done
#16 exporting attestation manifest sha256:1f0afc1a68003ff2ce02582b366c9326c3e476aaabcdaa830d3a86a57d95bbd9
#16 exporting attestation manifest sha256:1f0afc1a68003ff2ce02582b366c9326c3e476aaabcdaa830d3a86a57d95bbd9 0.0s done
#16 exporting manifest sha256:ed2301bb69730e8ae287004665f0db8f615f06aa99fc9146675104ed25bda0fd 0.0s done
#16 exporting config sha256:883c4afb1ea39ce5086851996f7623f65b85a45a033831a84cb43f5c3d445f28 0.0s done
#16 exporting attestation manifest sha256:28a4fe35311bb6f077576a1173d4a8d8d3d99da2294e6979ae78a1752206b200 0.1s done
#16 exporting manifest list sha256:7d0d83dd21f32f33fd5990d2877007719464fd75331d76d38f62e27e40b7be26
#16 exporting manifest list sha256:7d0d83dd21f32f33fd5990d2877007719464fd75331d76d38f62e27e40b7be26 0.0s done
#16 naming to docker.io/demo/plateformes:cross done
#16 unpacking to docker.io/demo/plateformes:cross 0.0s done
#16 DONE 0.6s
real 0m8.883s
user 0m0.044s
sys 0m0.035s

L'étape de compilation s'appelle maintenant [linux/amd64->arm64 build 4/4] : la flèche indique qu'elle s'exécute sur amd64 pour viser arm64. Elle dure 7,4 s et la construction entière 8,9 s. La sortie montre aussi la compilation pour amd64, [linux/amd64 build 4/4], sans flèche puisqu'elle vise la machine elle-même : 7,4 s également.

MéthodeCompilation arm64Construction des deux plateformes
Émulée (Dockerfile.emul)51,2 s56,8 s
Croisée (Dockerfile)7,4 s8,9 s

Ces durées viennent d'une seule exécution sur la VM Debian 13 amd64 de la mesure, chaque construction étant lancée avec --no-cache : lisez-les comme un ordre de grandeur et mesurez les vôtres. Pour savoir si les deux méthodes donnent le même programme, copiez les binaires hors des images et comparez leurs empreintes.

Fenêtre de terminal
mkdir -p out-cross-arm64 && docker create --platform linux/arm64 --name x-cross-arm64 demo/plateformes:cross >/dev/null && docker cp x-cross-arm64:/app out-cross-arm64/app && docker rm x-cross-arm64 >/dev/null && file out-cross-arm64/app && sha256sum out-cross-arm64/app
mkdir -p out-emul-arm64 && docker create --platform linux/arm64 --name x-emul-arm64 demo/plateformes:emul >/dev/null && docker cp x-emul-arm64:/app out-emul-arm64/app && docker rm x-emul-arm64 >/dev/null && file out-emul-arm64/app && sha256sum out-emul-arm64/app
out-cross-arm64/app: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, Go BuildID=fRa4Z91i4F52WwsekNq2/BXCI_4KDnM1hvHaxGZBI/nrGeYniJwRRWDvAWaSZ4/q3m2sBdQv68dkukXKbLW, BuildID[sha1]=4df9c0d5dcf12b43ebc850eb2dea127d64d7fc9f, with debug_info, not stripped
c0947d04b45aca0bd756ad70b45fad837d8a40e87c55343f769dd4fff618624d out-cross-arm64/app
out-emul-arm64/app: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, Go BuildID=fRa4Z91i4F52WwsekNq2/BXCI_4KDnM1hvHaxGZBI/nrGeYniJwRRWDvAWaSZ4/q3m2sBdQv68dkukXKbLW, BuildID[sha1]=4df9c0d5dcf12b43ebc850eb2dea127d64d7fc9f, with debug_info, not stripped
c0947d04b45aca0bd756ad70b45fad837d8a40e87c55343f769dd4fff618624d out-emul-arm64/app

La commande file lit l'en-tête du fichier : ARM aarch64 confirme l'architecture sans exécuter le programme. Les deux empreintes sont identiques (c0947d04…) : sur ce programme, la compilation émulée et la compilation croisée donnent le même binaire, octet pour octet. La compilation croisée ne dégrade donc pas le résultat ici ; elle ne dit rien d'un programme qui dépendrait de bibliothèques C ou dont le RUN final exécuterait du code de la cible, cas que la leçon n'a pas mesurés.

Une troisième stratégie existe : compiler chaque plateforme sur un nœud natif (une machine arm64 pour l'arm64), puis assembler l'index avec docker buildx imagetools create. La leçon ne l'a pas mesurée, faute de machine arm64 : pour la mettre en œuvre, suivez la documentation Docker sur les builds multi-plateformes. La page Cache BuildKit en CI et promotion d'image entre registres emploie bien imagetools create, mais pour copier un index existant d'un registre à l'autre, pas pour assembler des builds natifs.

Pourquoi BUILDPLATFORM seul produit-il un binaire faux ?

Section intitulée « Pourquoi BUILDPLATFORM seul produit-il un binaire faux ? »

Le piège naît d'un oubli : garder FROM --platform=$BUILDPLATFORM sans passer GOOS et GOARCH à go build. Go compile alors pour la machine qui construit, c'est-à-dire pour amd64, quelle que soit la plateforme visée.

Dockerfile.piege
FROM --platform=$BUILDPLATFORM golang:1.27-alpine@sha256:d200d2dd76b03a99b52651fd1325aff7dcd35422c35c01bac56571b0507f3deb AS build
WORKDIR /src
COPY go.mod main.go ./
RUN CGO_ENABLED=0 go build -o /out/app .
FROM alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]
Fenêtre de terminal
time docker buildx build --no-cache --platform linux/amd64,linux/arm64 -f Dockerfile.piege -t demo/plateformes:piege --load --progress=plain . 2>&1 | tail -25
[...]
#12 [linux/amd64 build 4/4] RUN CGO_ENABLED=0 go build -o /out/app .
#12 DONE 4.1s
[...]
#15 DONE 0.7s
real 0m5.485s

La construction réussit, plus vite que les deux autres (une seule compilation, amd64, sert pour les deux plateformes : l'étape s'appelle [linux/amd64 build 4/4], sans flèche). L'index est correct : docker image inspect annonce bien linux/arm64 pour la seconde variante. C'est son contenu qui est faux.

Fenêtre de terminal
docker image inspect --platform linux/arm64 --format '{{.Os}}/{{.Architecture}} id={{.Id}}' demo/plateformes:piege
docker run --rm --platform linux/arm64 demo/plateformes:piege
mkdir -p out-piege-arm64 && docker create --platform linux/arm64 --name x-piege-arm64 demo/plateformes:piege >/dev/null && docker cp x-piege-arm64:/app out-piege-arm64/app && docker rm x-piege-arm64 >/dev/null && file out-piege-arm64/app && sha256sum out-piege-arm64/app
linux/arm64 id=sha256:da09e9ebf5a8f6434dbd90e7691851e597358a682995e3139263d5a58d2c31c9
GOOS=linux GOARCH=amd64 app=semrofetalp
out-piege-arm64/app: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=8Hx6toIjGIVVZhE10x0l/yS1TKDtgP0hFNYOpW5iN/6pFQZ3Qn0lvYNzYf84bn/joVmo_R_xxFaEsEbCtzM, BuildID[sha1]=0eecf3c551407a2c4e8bd2386769f1de3d8aa2a5, with debug_info, not stripped
f8e3629bcece470fd063e4fafe7ba50ed7de31b52babc68166781a7efd011c43 out-piege-arm64/app

Lisez les trois sorties ensemble. Le manifeste dit linux/arm64, mais le programme répond GOARCH=amd64, et file identifie un exécutable x86-64.

Sur cette VM amd64, la variante « arm64 » démarre sans erreur, parce que l'hôte exécute directement le binaire amd64 qu'elle contient. Sur un hôte arm64, le comportement n'a pas été mesuré. Seuls un lancement de chaque variante, ou un file sur chaque binaire, révèlent le défaut ; aucun message de construction n'en parle.

Un index se publie avec --push, qui envoie le résultat à un registre au lieu de le charger en local. Pour l'essai, lancez un registre local qui n'écoute que sur la machine, sur le port 5055. L'image registry:3 est épinglée par digest.

Fenêtre de terminal
docker run -d --name demo-registry -p 127.0.0.1:5055:5000 registry:3@sha256:d4032d4cc323c1731c707931ecada07a53076eac38e3b61ae57533f170f2c6c3 | cut -c1-12
sleep 2
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:5055/v2/
edf8c2d24450
200

Au premier lancement, les lignes du tirage de l'image registry s'affichent avant l'identifiant. L'identifiant tronqué et le code 200 de l'adresse /v2/ montrent que le registre répond. Publiez ensuite l'index, avec le Dockerfile de compilation croisée (la progression de la construction n'est pas reproduite), puis lisez-le. Les digests de cette leçon seront différents chez vous : reprenez les vôtres, lus dans l'index.

Fenêtre de terminal
docker buildx build --platform linux/amd64,linux/arm64 -f Dockerfile -t 127.0.0.1:5055/demo/plateformes:1.0 --push --progress=plain . 2>&1 | tail -12
docker buildx imagetools inspect 127.0.0.1:5055/demo/plateformes:1.0
Name: 127.0.0.1:5055/demo/plateformes:1.0
MediaType: application/vnd.oci.image.index.v1+json
Digest: sha256:c436d1a51d9234e3a0e81a8f9d7ad8aefd86ea3bf325a9ab6e5850addb409352
Manifests:
Name: 127.0.0.1:5055/demo/plateformes:1.0@sha256:d75b98894278e727eb1752efc3677cb3f242c13191d427acb33323b4493c4202
MediaType: application/vnd.oci.image.manifest.v1+json
Platform: linux/amd64
Name: 127.0.0.1:5055/demo/plateformes:1.0@sha256:c833aa8b36441c1ecdc60a928aef3f2520442958e7d5195d71e3c8b0459a5e5f
MediaType: application/vnd.oci.image.manifest.v1+json
Platform: linux/arm64
Name: 127.0.0.1:5055/demo/plateformes:1.0@sha256:4e7ae54f13010e50cc1e768c6f0b871808a1ed6a278db43fb72bcbd32b1a24b7
MediaType: application/vnd.oci.image.manifest.v1+json
Platform: unknown/unknown
Annotations:
vnd.docker.reference.digest: sha256:d75b98894278e727eb1752efc3677cb3f242c13191d427acb33323b4493c4202
vnd.docker.reference.type: attestation-manifest
Name: 127.0.0.1:5055/demo/plateformes:1.0@sha256:5b14946bee721bd75564a61ef3ae4fcc1ece8a7c565ff1cee1ccf93dc28d9367
MediaType: application/vnd.oci.image.manifest.v1+json
Platform: unknown/unknown
Annotations:
vnd.docker.reference.type: attestation-manifest
vnd.docker.reference.digest: sha256:c833aa8b36441c1ecdc60a928aef3f2520442958e7d5195d71e3c8b0459a5e5f

Deux plateformes, quatre manifestes. Les deux premiers sont les variantes réelles. Les deux autres, de plateforme unknown/unknown, sont des attestations de provenance, des documents qui décrivent comment l'image a été construite. Buildx les demande par défaut et BuildKit les produit (l'option --provenance=false, vue plus bas, les supprime). L'annotation vnd.docker.reference.digest désigne le manifeste décrit, et vnd.docker.reference.type vaut attestation-manifest.

Le schéma montre cette structure. Suivez les traits pleins, qui mènent à ce qui s'exécute, puis les traits pointillés, qui relient chaque attestation au manifeste qu'elle décrit.

Un tag mène à un index OCI de quatre entrées : un manifeste linux/amd64 et un manifeste linux/arm64, chacun relié à ses couches et à son binaire, et deux attestations de plateforme unknown/unknown, chacune décrivant l'un des deux manifestes.Tag demo/plateformes:1.0Index OCI4 entréesManifeste linux/amd64Manifeste linux/arm64Attestation unknown/unknowndécrit le manifeste amd64Attestation unknown/unknowndécrit le manifeste arm64Couches et binaire amd64Couches et binaire arm64
Seules les deux variantes réelles contiennent un programme ; les attestations, en pointillé, décrivent un manifeste sans pouvoir être exécutées. Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

Un script de quelques lignes sépare les variantes des attestations. Il lit l'index brut et affiche, pour chaque entrée, son digest, sa plateforme et son type.

digests.py
import json, sys
# usage: digests.py [os/arch] (stdin = index brut) ; sans argument, liste tout
idx = json.load(sys.stdin)
want = sys.argv[1] if len(sys.argv) > 1 else None
print("entrees:", len(idx["manifests"]), file=sys.stderr)
for m in idx["manifests"]:
p = m.get("platform", {})
plat = p.get("os", "?") + "/" + p.get("architecture", "?")
ann = m.get("annotations", {})
if want:
if plat == want:
print(m["digest"])
else:
print(m["digest"], plat, ann.get("vnd.docker.reference.type", "-"), ann.get("vnd.docker.reference.digest", "-")[:19])
Fenêtre de terminal
docker buildx imagetools inspect --raw 127.0.0.1:5055/demo/plateformes:1.0 | python3 digests.py
entrees: 4
sha256:d75b98894278e727eb1752efc3677cb3f242c13191d427acb33323b4493c4202 linux/amd64 - -
sha256:c833aa8b36441c1ecdc60a928aef3f2520442958e7d5195d71e3c8b0459a5e5f linux/arm64 - -
sha256:4e7ae54f13010e50cc1e768c6f0b871808a1ed6a278db43fb72bcbd32b1a24b7 unknown/unknown attestation-manifest sha256:d75b98894278
sha256:5b14946bee721bd75564a61ef3ae4fcc1ece8a7c565ff1cee1ccf93dc28d9367 unknown/unknown attestation-manifest sha256:c833aa8b3644

Ne comptez jamais les entrées d'un index pour compter les plateformes : ici 4 entrées pour 2 plateformes. Lisez la plateforme de chaque entrée. Pour obtenir un index qui ne contient que les variantes, désactivez la provenance avec --provenance=false.

Fenêtre de terminal
docker buildx build --platform linux/amd64,linux/arm64 -f Dockerfile -t 127.0.0.1:5055/demo/plateformes:1.0-nop --provenance=false --push --progress=plain . 2>&1 | tail -6
docker buildx imagetools inspect --raw 127.0.0.1:5055/demo/plateformes:1.0-nop | python3 digests.py
entrees: 2
sha256:d75b98894278e727eb1752efc3677cb3f242c13191d427acb33323b4493c4202 linux/amd64 - -
sha256:c833aa8b36441c1ecdc60a928aef3f2520442958e7d5195d71e3c8b0459a5e5f linux/arm64 - -

Les deux variantes ont les mêmes digests que dans l'index précédent : seul l'index change. Le registre liste maintenant les deux tags.

Fenêtre de terminal
curl -s http://127.0.0.1:5055/v2/demo/plateformes/tags/list
{"name":"demo/plateformes","tags":["1.0","1.0-nop"]}

Désactiver la provenance supprime une information utile à l'audit de la chaîne de construction. Faites-le quand un outil consommateur ne comprend pas les entrées unknown/unknown, pas par défaut.

Pourquoi un builder docker-container n'atteint-il pas 127.0.0.1 ?

Section intitulée « Pourquoi un builder docker-container n'atteint-il pas 127.0.0.1 ? »

Le builder par défaut a publié sur 127.0.0.1:5055 sans difficulté. Un builder docker-container échoue sur la même adresse. Recréez le builder multi et tentez la même publication.

Fenêtre de terminal
docker buildx create --name multi --driver docker-container \
--driver-opt image=moby/buildkit:v0.34.0@sha256:b059f8d7226d0b326bc871af489a5dddf65e36af4d20d14251b072974d9ee87c --use
docker buildx build --builder multi --platform linux/amd64,linux/arm64 -f Dockerfile -t 127.0.0.1:5055/demo/plateformes:cont --push . 2>&1 | tail -3
multi
> exporting to image:
------
ERROR: failed to build: failed to solve: failed to push 127.0.0.1:5055/demo/plateformes:cont: failed to do request: Head "http://127.0.0.1:5055/v2/demo/plateformes/blobs/sha256:6f4ccdb83c2f8f3bde1bc6328322cb52da372ec2260b9ee8a5295f42e2155bff": dial tcp 127.0.0.1:5055: connect: connection refused

Le message connection refused sur 127.0.0.1:5055 vient du fait que le builder tourne dans un conteneur, sur le réseau bridge de Docker. Pour lui, 127.0.0.1 est son propre conteneur, où personne n'écoute sur ce port, et non votre machine. Il faut recréer le builder en lui donnant le réseau de l'hôte avec --driver-opt network=host. L'option ne se change pas sur un builder existant.

Fenêtre de terminal
docker buildx use default
docker buildx rm multi
docker buildx create --name multi --driver docker-container \
--driver-opt network=host --driver-opt image=moby/buildkit:v0.34.0@sha256:b059f8d7226d0b326bc871af489a5dddf65e36af4d20d14251b072974d9ee87c --use
docker buildx build --builder multi --platform linux/amd64,linux/arm64 -f Dockerfile -t 127.0.0.1:5055/demo/plateformes:cont --push . 2>&1 | tail -3
multi removed
multi
#17 pushing manifest for 127.0.0.1:5055/demo/plateformes:cont@sha256:39ce05f2618949d78e22273cde8dd205bc40599a60070009c054fb3137c388c0
#17 pushing manifest for 127.0.0.1:5055/demo/plateformes:cont@sha256:39ce05f2618949d78e22273cde8dd205bc40599a60070009c054fb3137c388c0 0.1s done
#17 DONE 0.9s

La publication aboutit. En contrepartie, ce builder partage le réseau de la machine et peut joindre ses services locaux : gardez cette option pour les essais, et retirez le builder ensuite.

Fenêtre de terminal
docker buildx use default
docker buildx rm multi

La commande rm répond multi removed.

Un tag d'index n'est pas une preuve : il peut cacher une variante fausse. La preuve consiste à tirer chaque variante par le digest de son manifeste, lu dans l'index, puis à l'exécuter. Lisez d'abord les deux digests dans l'index publié, avec digests.py et son argument de plateforme, et rangez-les dans deux variables : aucun digest ne s'écrit à la main.

Fenêtre de terminal
cd ~/multiarch
AMD=$(docker buildx imagetools inspect --raw 127.0.0.1:5055/demo/plateformes:1.0 | python3 digests.py linux/amd64)
ARM=$(docker buildx imagetools inspect --raw 127.0.0.1:5055/demo/plateformes:1.0 | python3 digests.py linux/arm64)
echo "AMD=$AMD ARM=$ARM"
entrees: 4
entrees: 4
AMD=sha256:d75b98894278e727eb1752efc3677cb3f242c13191d427acb33323b4493c4202 ARM=sha256:c833aa8b36441c1ecdc60a928aef3f2520442958e7d5195d71e3c8b0459a5e5f

Les digests des sorties qui suivent sont ceux de la mesure : les vôtres diffèrent, et les variables les portent à votre place. La ligne entrees: 4 part sur la sortie d'erreur, elle s'affiche mais n'entre pas dans la variable. Les variables n'existent que dans ce terminal : si vous le fermez, rejouez ce bloc tant que le registre tourne.

Supprimez ensuite les copies locales, pour que le test parte du registre.

Fenêtre de terminal
docker image rm demo/plateformes:cross demo/plateformes:emul demo/plateformes:piege demo/plateformes:run-arm64 127.0.0.1:5055/demo/plateformes:1.0 127.0.0.1:5055/demo/plateformes:1.0-nop

La commande affiche une ligne Untagged: par image retirée. Tirez et exécutez ensuite la variante amd64, puis la variante arm64.

Fenêtre de terminal
docker pull 127.0.0.1:5055/demo/plateformes@$AMD | tail -3
docker run --rm --platform linux/amd64 127.0.0.1:5055/demo/plateformes@$AMD
docker image inspect --format '{{.Architecture}}' 127.0.0.1:5055/demo/plateformes@$AMD
Digest: sha256:d75b98894278e727eb1752efc3677cb3f242c13191d427acb33323b4493c4202
Status: Downloaded newer image for 127.0.0.1:5055/demo/plateformes@sha256:d75b98894278e727eb1752efc3677cb3f242c13191d427acb33323b4493c4202
127.0.0.1:5055/demo/plateformes@sha256:d75b98894278e727eb1752efc3677cb3f242c13191d427acb33323b4493c4202
GOOS=linux GOARCH=amd64 app=semrofetalp
amd64
Fenêtre de terminal
docker pull 127.0.0.1:5055/demo/plateformes@$ARM | tail -3
docker run --rm --platform linux/arm64 127.0.0.1:5055/demo/plateformes@$ARM
docker image inspect --format '{{.Architecture}}' 127.0.0.1:5055/demo/plateformes@$ARM
Digest: sha256:c833aa8b36441c1ecdc60a928aef3f2520442958e7d5195d71e3c8b0459a5e5f
Status: Downloaded newer image for 127.0.0.1:5055/demo/plateformes@sha256:c833aa8b36441c1ecdc60a928aef3f2520442958e7d5195d71e3c8b0459a5e5f
127.0.0.1:5055/demo/plateformes@sha256:c833aa8b36441c1ecdc60a928aef3f2520442958e7d5195d71e3c8b0459a5e5f
GOOS=linux GOARCH=arm64 app=semrofetalp
arm64

Chaque exécution rend GOARCH égal à l'architecture annoncée par docker image inspect : le programme de la variante est celui de sa plateforme. Si l'une des deux lignes GOARCH avait été fausse, comme dans la variante du piège, vous l'auriez vu ici.

Que fait docker run --platform selon ce qui est déjà en local ?

Section intitulée « Que fait docker run --platform selon ce qui est déjà en local ? »

La commande docker run --platform ne se comporte pas de la même façon selon ce que Docker a déjà en local pour cette image. Trois situations ont été mesurées, avec Docker 29.9.2 et le magasin containerd.

Dans la première, vous avez tiré un tag d'index avec une seule plateforme. Supprimez les copies par digest, puis tirez uniquement la variante amd64 de l'index sans attestations.

Fenêtre de terminal
docker image rm 127.0.0.1:5055/demo/plateformes@$ARM 127.0.0.1:5055/demo/plateformes@$AMD
docker pull --platform linux/amd64 127.0.0.1:5055/demo/plateformes:1.0-nop
docker image ls --tree | sed -n '1p;/plateformes/,+3p'

L'arborescence se lit avec docker image ls --tree : la variante arm64 y figure avec 0B, c'est-à-dire déclarée par l'index mais absente du disque.

IMAGE ID DISK USAGE CONTENT SIZE EXTRA
127.0.0.1:5055/demo/plateformes:1.0-nop e804613147cb 16.8MB 5.3MB
├─ linux/amd64 d75b98894278 16.8MB 5.3MB
└─ linux/arm64 c833aa8b3644 0B 0B

Lancez maintenant la variante arm64, qui n'est pas en local.

Fenêtre de terminal
docker run --rm --platform linux/arm64 127.0.0.1:5055/demo/plateformes:1.0-nop
Unable to find image '127.0.0.1:5055/demo/plateformes:1.0-nop' locally
1.0-nop: Pulling from demo/plateformes
e5c2b96a0c80: Pulling fs layer
d3531b7a4f21: Pulling fs layer
e5c2b96a0c80: Already exists
d3531b7a4f21: Already exists
e5c2b96a0c80: Pull complete
Digest: sha256:e804613147cb4fb45c7ef6f9619df1abd15a2b184e923469f2dc5569e4722b61
Status: Image is up to date for 127.0.0.1:5055/demo/plateformes:1.0-nop
GOOS=linux GOARCH=arm64 app=semrofetalp

Docker a tiré la couche manquante de la variante arm64 et l'a lancée. Le message est trompeur : Status: Image is up to date désigne l'index, déjà présent, et non la variante, qui vient d'être téléchargée (la ligne e5c2b96a0c80: Pull complete en témoigne). Le lancement sans --platform reste sur la plateforme de la machine.

Fenêtre de terminal
docker run --rm 127.0.0.1:5055/demo/plateformes:1.0-nop
GOOS=linux GOARCH=amd64 app=semrofetalp

Dans la deuxième situation, l'image locale est un manifeste unique, tiré par son digest. Aucun index ne permet de trouver une autre variante. Supprimez le tag d'index, tirez la variante amd64 par digest et demandez arm64.

Fenêtre de terminal
docker image rm 127.0.0.1:5055/demo/plateformes:1.0-nop
docker pull 127.0.0.1:5055/demo/plateformes@$AMD | tail -2
docker run --rm --platform linux/arm64 127.0.0.1:5055/demo/plateformes@$AMD
echo "code=$?"
Status: Downloaded newer image for 127.0.0.1:5055/demo/plateformes@sha256:d75b98894278e727eb1752efc3677cb3f242c13191d427acb33323b4493c4202
127.0.0.1:5055/demo/plateformes@sha256:d75b98894278e727eb1752efc3677cb3f242c13191d427acb33323b4493c4202
Unable to find image '127.0.0.1:5055/demo/plateformes@sha256:d75b98894278e727eb1752efc3677cb3f242c13191d427acb33323b4493c4202' locally
127.0.0.1:5055/demo/plateformes@sha256:d75b98894278e727eb1752efc3677cb3f242c13191d427acb33323b4493c4202: Pulling from demo/plateformes
Digest: sha256:d75b98894278e727eb1752efc3677cb3f242c13191d427acb33323b4493c4202
Status: Image is up to date for 127.0.0.1:5055/demo/plateformes@sha256:d75b98894278e727eb1752efc3677cb3f242c13191d427acb33323b4493c4202
docker: Error response from daemon: image with reference 127.0.0.1:5055/demo/plateformes@sha256:d75b98894278e727eb1752efc3677cb3f242c13191d427acb33323b4493c4202 was found but does not provide the specified platform (linux/arm64)
Run 'docker run --help' for more information
code=125

Cette fois Docker échoue avec le code 125 et le message was found but does not provide the specified platform (linux/arm64). Un digest de manifeste désigne une seule plateforme : il n'y a pas de variante à aller chercher. Le même message s'observe, avec --pull=never, sur une image locale à manifeste unique : mesuré sur l'image Alpine tirée en amd64 seule.

Dans la troisième situation, l'image vient d'être construite en local pour une seule plateforme, sous un tag qui ne correspond à aucun registre.

Fenêtre de terminal
docker buildx build --platform linux/amd64 -f Dockerfile -t demo/plateformes:amd64only --load . 2>&1 | tail -3
docker run --rm --platform linux/arm64 demo/plateformes:amd64only
echo "code=$?"
#12 naming to docker.io/demo/plateformes:amd64only done
#12 unpacking to docker.io/demo/plateformes:amd64only 0.0s done
#12 DONE 0.1s
Unable to find image 'demo/plateformes:amd64only' locally
docker: Error response from daemon: pull access denied for demo/plateformes, repository does not exist or may require 'docker login'
Run 'docker run --help' for more information
code=125

Docker ne trouve pas la plateforme demandée en local, tente de la tirer depuis le registre que le nom désigne (Docker Hub, pour demo/plateformes) et reçoit pull access denied. Le code 125 est encore celui de Docker lui-même, pas du programme. Le tableau résume les trois cas.

Ce qui est en localCe que fait docker run --platform linux/arm64Ce que vous lisez
Tag d'index tiré en amd64 seulTire la variante arm64 et la lanceImage is up to date, puis GOARCH=arm64
Manifeste unique amd64, référence par digestÉchoue : l'image ne fournit pas cette plateformeCode 125, was found but does not provide the specified platform
Image construite en local pour amd64 seulCherche la plateforme dans un registre qui ne l'a pasCode 125, pull access denied

Pour les deux premiers cas, la parade tient en deux gestes : tirer la plateforme voulue avec docker pull --platform, puis vérifier l'architecture avec docker image inspect --platform et le lancement lui-même, plutôt que de se fier au message Image is up to date. Pour le troisième, construisez la plateforme voulue, ou publiez l'index et tirez-le.

Comment déclarer les plateformes que vous n'avez pas rejouées ?

Section intitulée « Comment déclarer les plateformes que vous n'avez pas rejouées ? »

Tout ce que cette leçon a exécuté l'a été sur une seule machine amd64, avec l'arm64 émulé par QEMU. Exécuter l'image sous émulation prouve que le binaire arm64 démarre et répond ; cela ne prouve pas son comportement sur un processeur arm64 réel, que la leçon n'a pas mesuré. Une publication honnête dit, plateforme par plateforme, ce qui a été construit, ce qui a été exécuté et où.

PlateformeConstruiteExécutéeOùCe que cela prouve
linux/amd64ouiouien natif, sur la VM de mesurele binaire x86-64 répond GOARCH=amd64
linux/arm64ouiouisous QEMU, sur la même VMle binaire ARM aarch64 démarre et répond GOARCH=arm64 ; rien sur un hôte arm64
autres (386, s390x)nonnonrien

Une plateforme que vous annoncez dans l'index sans l'avoir exécutée doit figurer comme telle dans ce tableau, ou ne pas être publiée. La construction de linux/s390x avec Dockerfile.run ou Dockerfile.emul, qui exécutent un RUN sous la plateforme cible, échoue d'ailleurs sur cette machine, parce que seul l'émulateur arm64 y est enregistré (voir le tableau de dépannage). Pour qualifier une plateforme, exécutez-la sur du matériel de cette architecture, par exemple depuis une CI qui dispose de machines arm64. Cette leçon n'a pas rejoué ce cas.

Deux guides prolongent cette leçon. Pour un cluster qui mélange les deux architectures, voir Mixer des nœuds ARM et AMD64 dans un cluster k3s ; pour orchestrer plusieurs constructions depuis un fichier, voir Docker Bake.

Que faire quand une construction ou un lancement multi-plateforme échoue ?

Section intitulée « Que faire quand une construction ou un lancement multi-plateforme échoue ? »

Chaque erreur du tableau a été provoquée lors des mesures, avec Docker 29.9.2 et Buildx 0.38.1. Lisez la colonne de gauche comme le symptôme que vous voyez, et la dernière comme le geste qui le corrige.

SymptômeCauseSolution
exec /bin/uname: exec format error, code 255, au docker run --platform linux/arm64Aucun émulateur arm64 enregistré dans binfmt_miscEnregistrer l'émulation avec tonistiigi/binfmt et --install arm64
exec /bin/sh: exec format error et exit code: 255 à une étape RUN d'une constructionLe RUN exécute un programme de la plateforme cible, sans émulateurEnregistrer l'émulation, ou compiler en croisé et ne garder que des COPY dans l'étape finale
La construction --platform linux/s390x d'un Dockerfile qui exécute un RUN sous la plateforme cible échoue en 255 alors que l'arm64 fonctionneSeul qemu-aarch64 est enregistréEnregistrer l'émulateur de cette architecture, ou ne pas la viser
failed to push 127.0.0.1:5055/…: connection refused avec un builder docker-containerLe builder s'exécute sur le réseau bridge : 127.0.0.1 est son propre conteneurRecréer le builder avec --driver-opt network=host
Status: Image is up to date alors que la variante arm64 vient d'être téléchargéeLe message concerne l'index, déjà local, pas la varianteVérifier avec docker image ls --tree et docker image inspect --platform
was found but does not provide the specified platform (linux/arm64), code 125L'image locale est un manifeste unique d'une autre plateformeTirer l'index avec docker pull --platform linux/arm64, ou le digest de la bonne variante
pull access denied for demo/plateformes, code 125, à un docker run --platformL'image locale n'a pas la plateforme et Docker cherche un registre qui ne la contient pasConstruire la plateforme voulue, ou publier l'index et le tirer
Les deux variantes répondent GOARCH=amd64, file annonce x86-64 partoutFROM --platform=$BUILDPLATFORM sans GOOS=$TARGETOS GOARCH=$TARGETARCH dans go buildPasser TARGETOS et TARGETARCH à la compilation, puis lancer chaque variante
4 entrées dans l'index pour 2 plateformesDeux attestations de provenance, de plateforme unknown/unknownLire la plateforme de chaque entrée ; --provenance=false si un outil les refuse

La démonstration laisse un registre, des images, une émulation, un builder éventuel, des fichiers et du cache. Retirez-les dans cet ordre, sans rm -r : chaque suppression nomme ce qu'elle retire. La variable AMD sert à supprimer une image plus bas : si vous avez changé de terminal, recalculez-la avant d'arrêter le registre. Commencez par le registre et le builder.

Fenêtre de terminal
docker buildx ls
docker volume ls
docker rm -f -v demo-registry
docker image rm registry@sha256:d4032d4cc323c1731c707931ecada07a53076eac38e3b61ae57533f170f2c6c3
docker volume ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default* docker
\_ default \_ default running v0.34.0 linux/amd64 (+3), linux/arm64
DRIVER VOLUME NAME
local f7ca5e2a38bef88fa3326fea36e7e4e3c573b507460c1127ecae2d5489df1916
demo-registry
Untagged: registry@sha256:d4032d4cc323c1731c707931ecada07a53076eac38e3b61ae57533f170f2c6c3
Deleted: sha256:d4032d4cc323c1731c707931ecada07a53076eac38e3b61ae57533f170f2c6c3
DRIVER VOLUME NAME

L'image registry:3 déclare un volume anonyme, que docker rm -f seul laisse en place. L'option -v le retire avec le conteneur : comparez les deux listes de docker volume ls, le volume du registre disparaît de la seconde et vos autres volumes y restent. Si docker buildx ls montre encore le builder multi, retirez-le ; s'il n'existe plus, docker buildx rm répond no builder "multi" found, sans conséquence.

Fenêtre de terminal
docker buildx use default
docker buildx rm multi

Retirez ensuite l'émulation, puis les images de la leçon. Ne lancez la première commande que si la liste relevée au début (le ls de /proc/sys/fs/binfmt_misc) ne contenait pas déjà qemu-aarch64 : sinon, vous retireriez une émulation qui existait avant la leçon.

Fenêtre de terminal
docker run --privileged --rm tonistiigi/binfmt:qemu-v10.2.3-68@sha256:400a4873b838d1b89194d982c45e5fb3cda4593fbfd7e08a02e76b03b21166f0 --uninstall qemu-aarch64
ls /proc/sys/fs/binfmt_misc
docker image rm demo/plateformes:amd64only 127.0.0.1:5055/demo/plateformes@$AMD tonistiigi/binfmt:qemu-v10.2.3-68@sha256:400a4873b838d1b89194d982c45e5fb3cda4593fbfd7e08a02e76b03b21166f0 moby/buildkit:v0.34.0@sha256:b059f8d7226d0b326bc871af489a5dddf65e36af4d20d14251b072974d9ee87c golang@sha256:d200d2dd76b03a99b52651fd1325aff7dcd35422c35c01bac56571b0507f3deb
uninstalling: qemu-aarch64 OK
{
"supported": [
"linux/amd64",
"linux/amd64/v2",
"linux/amd64/v3",
"linux/386"
],
"emulators": [
"python3.13"
]
}
python3.13
register
status
Untagged: demo/plateformes:amd64only
Deleted: sha256:f8dd24ec566157b5199cf2be50e75e782e8f4f58342249ba4684ac552dde2e99
Untagged: 127.0.0.1:5055/demo/plateformes@sha256:d75b98894278e727eb1752efc3677cb3f242c13191d427acb33323b4493c4202
Deleted: sha256:d75b98894278e727eb1752efc3677cb3f242c13191d427acb33323b4493c4202
Untagged: tonistiigi/binfmt@sha256:400a4873b838d1b89194d982c45e5fb3cda4593fbfd7e08a02e76b03b21166f0
Deleted: sha256:400a4873b838d1b89194d982c45e5fb3cda4593fbfd7e08a02e76b03b21166f0
Untagged: moby/buildkit@sha256:b059f8d7226d0b326bc871af489a5dddf65e36af4d20d14251b072974d9ee87c
Deleted: sha256:b059f8d7226d0b326bc871af489a5dddf65e36af4d20d14251b072974d9ee87c
Untagged: golang@sha256:d200d2dd76b03a99b52651fd1325aff7dcd35422c35c01bac56571b0507f3deb
Deleted: sha256:d200d2dd76b03a99b52651fd1325aff7dcd35422c35c01bac56571b0507f3deb

Contrôlez l'état du noyau avec ls /proc/sys/fs/binfmt_misc, qui ne doit plus lister qemu-aarch64. Ne vous fiez pas à docker buildx ls pour cela : après le retrait, le builder par défaut y affichait encore linux/arm64 dans les mesures. Cette liste est restée en cache jusqu'au redémarrage du moteur ; après sudo systemctl restart docker, elle est revenue à linux/amd64, linux/amd64/v2 et linux/amd64/v3. Le vrai contrôle reste /proc/sys/fs/binfmt_misc.

Attention : docker builder prune -af vide tout le cache de construction du builder, pas seulement celui de cette leçon. Sautez cette commande si d'autres projets en dépendent.

Fenêtre de terminal
docker builder prune -af
docker image ls
ID RECLAIMABLE SIZE LAST ACCESSED
qqfegccgz1itogk5io3szu2lf* true 36.43MB About a minute ago
2e807td3hxxvpgioms1xqm33l* true 36.4MB About a minute ago
[...]
Total: 959.4MB
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
alpine:latest be2656061b53 26.6MB 8.22MB
alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a be2656061b53 26.6MB 8.22MB

La ligne alpine:latest appartient à l'état de départ de la machine de mesure : elle n'a pas été créée par la leçon, et la vôtre peut différer.

Supprimez enfin les fichiers de la leçon, un par un, puis le dossier vide.

Fenêtre de terminal
cd ~/multiarch && for d in out-*; do rm $d/app; rmdir $d; done; rm Dockerfile Dockerfile.emul Dockerfile.piege Dockerfile.run go.mod main.go digests.py; cd ~; rmdir multiarch

L'image Alpine peut rester : elle sert à d'autres leçons, et elle garde la variante arm64 tirée au début de celle-ci. Si vous voulez aussi la retirer, docker image rm suivi de sa référence suffit.

Le lab part d'un petit programme à livrer pour deux architectures. Vous publiez dans un registre local un index qui référence deux variantes réelles, puis vous exécutez chacune des deux. Seul l'état du système est contrôlé, avec dsoxlab check, jamais les commandes tapées : la validation lit l'index publié, vérifie qu'il contient bien les deux plateformes demandées, et contrôle à l'exécution que le binaire de chaque variante est celui de son architecture.

  1. Dans un Dockerfile, seules les instructions RUN lancent un programme de la plateforme cible : ce sont elles qui exigent l'émulation. FROM et COPY ne lancent aucun programme. Sans QEMU enregistré, un RUN d'une autre architecture échoue avec exec format error et le code 255.

  2. L'émulation s'enregistre et se retire avec docker run --privileged --rm tonistiigi/binfmt:qemu-v10.2.3-68@sha256:… --install arm64 puis --uninstall qemu-aarch64, et se vérifie dans /proc/sys/fs/binfmt_misc.

  3. Un builder docker-container s'ajoute et se retire (docker buildx create, docker buildx rm, puis docker image rm de l'image BuildKit épinglée). Sur le réseau bridge, il ne joint pas 127.0.0.1 de l'hôte : ajoutez --driver-opt network=host.

  4. Sur le magasin containerd, le builder par défaut construit deux plateformes avec --platform linux/amd64,linux/arm64 --load ; docker image ls --tree et docker image inspect --platform lisent chaque variante.

  5. La compilation croisée est plus rapide que l'émulation : 8,9 s contre 56,8 s sur la VM de mesure, avec FROM --platform=$BUILDPLATFORM, TARGETOS et TARGETARCH. BUILDPLATFORM sans GOARCH donne un index juste et un binaire faux.

  6. Ne comptez jamais les entrées d'un index : 4 entrées pour 2 plateformes, dont 2 attestations unknown/unknown. Tirez chaque variante par le digest de son manifeste et exécutez-la.

  7. docker run --platform dépend de ce qui est local : un index partiel tire la variante manquante sous un message Image is up to date trompeur, un manifeste unique donne le code 125. Tirez avec docker pull --platform, puis vérifiez à l'exécution.

  8. Un test sous émulation n'est pas une qualification sur du matériel natif : déclarez, plateforme par plateforme, ce qui a été construit, exécuté, et où.

Vérifiez que l'essentiel de ce guide 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