Aller au contenu

Images multi-architectures

200 Pratiquer ⏱ 55 min dockerbuildkitgopython

À la fin, vous saurez

  • Construire et publier une image multi-plateformes avec docker buildx build --platform
  • Expliquer BUILDPLATFORM, TARGETPLATFORM et leurs dérivés, et les utiliser pour compiler en croisé
  • Diagnostiquer un exec format error pendant la construction ou l'exécution
  • Construire une image Python multi-plateformes sans émulation, en téléchargeant les roues de la cible
  • Choisir entre compilation croisée, émulation QEMU et nœuds de construction natifs

Prérequis

Testé avec buildkit 0.33.0 docker 29.8.1 go 1.26.8 python 3.14.7 , vérifié le 1 octobre 2026

Pourquoi

Pendant longtemps, toutes les images ont été construites pour une seule architecture, amd64 (x86-64), et personne ne s'en souciait. Ce n'est plus le cas :

  • les postes de développement Apple sont en arm64 depuis 2020 ; une image amd64 y tourne sous émulation, lentement, ou pas du tout ;
  • les fournisseurs de cloud proposent des instances ARM, souvent moins chères à performance égale et plus sobres en énergie. Chez Scaleway, la zone fr-par-1 propose 22 types d'instances arm64 à côté de 103 types x64 :
$ scw instance server-type list zone=fr-par-1 -o json | jq -r '[.[] | .arch] | group_by(.) | map("\(.[0])=\(length)") | join(" ")'
arm64=22 x64=103
  • certains clients déploient sur du matériel embarqué ou en périphérie (edge) : passerelles industrielles en arm/v7, cartes riscv64.

Une image publiée pour une seule architecture ferme ces portes, ou les ouvre mal : le message exec format error (leçon 1 du cours précédent) est alors la seule documentation que reçoit l'utilisateur. Cette leçon montre comment publier une seule référence (signalements-export:1.0) qui sert automatiquement la bonne variante à chaque machine, et surtout comment la construire sans émuler les architectures étrangères, ce qui est la partie délicate.

Les concepts

L'index multi-plateformes

La leçon 7 du cours précédent a démonté l'index OCI d'alpine:3.22 : une liste de manifestes, un par plateforme (linux/amd64, linux/arm64, linux/arm/v7...). Quand un client télécharge une étiquette, il lit l'index, choisit le manifeste qui correspond à sa plateforme, et ne télécharge que ses couches. Publier une image multi-architectures, c'est donc construire une image par plateforme et les rassembler dans un index.

Une plateforme se note os/architecture[/variante] : linux/amd64, linux/arm64, linux/arm/v7 (ARM 32 bits de génération 7), linux/riscv64.

Plateforme de construction et plateforme cible

Dans une construction multi-plateformes, deux plateformes coexistent :

  • la plateforme de construction (BUILDPLATFORM) : celle de la machine qui exécute BuildKit, ici linux/amd64 ;
  • la plateforme cible (TARGETPLATFORM) : celle de l'image en cours de production, une par valeur de --platform.

BuildKit fournit ces valeurs comme arguments de construction prédéfinis, avec leurs composantes (TARGETOS, TARGETARCH, TARGETVARIANT, et leurs équivalents BUILD...). Par défaut, chaque étape s'exécute dans la plateforme cible : pour une cible arm64, une instruction RUN exécute des binaires arm64. Sur une machine amd64, c'est impossible sans aide.

Trois façons de construire pour une autre architecture

MéthodePrincipeCoûtQuand l'utiliser
Compilation croiséeLes étapes de construction tournent sur BUILDPLATFORM et produisent des fichiers pour TARGETPLATFORMRapide, natifLangages qui savent compiler pour une autre cible (Go, Rust, C avec la bonne chaîne d'outils), ou qui n'ont qu'à télécharger des binaires
Émulation QEMULe noyau de l'hôte exécute les binaires étrangers à travers l'émulateur QEMU, enregistré dans binfmt_miscLent (souvent 5 à 20 fois plus)Quand une étape doit vraiment exécuter du code de la cible et qu'aucune autre solution n'existe
Nœuds natifsUn builder BuildKit composé de plusieurs machines, une par architectureRapide, mais il faut des machinesEn CI à grande échelle, ou quand l'émulation est trop lente

La règle : compiler en croisé chaque fois que c'est possible, n'émuler qu'en dernier recours.

En pratique

Ce que la machine sait exécuter

$ docker buildx inspect default | grep -i platforms
Platforms:        linux/amd64, linux/amd64/v2, linux/amd64/v3
$ ls /proc/sys/fs/binfmt_misc/
python3.12
register
status

Le builder par défaut ne sait exécuter que des variantes d'amd64. Le répertoire binfmt_misc du noyau, qui associe des formats d'exécutables à des interpréteurs, ne contient aucun émulateur QEMU (l'entrée python3.12, installée par Ubuntu, sert à exécuter directement des fichiers .pyc).

Le piège : construire Signalements pour arm64 tel quel

$ docker buildx build --progress=plain --platform linux/amd64,linux/arm64 -t 127.0.0.1:5000/signalements:multi .
...
#15 [linux/arm64 2/6] RUN useradd --system --uid 10001 --no-create-home --shell /usr/sbin/nologin appli
#15 0.177 exec /bin/sh: exec format error
#15 ERROR: process "/bin/sh -c useradd --system --uid 10001 --no-create-home --shell /usr/sbin/nologin appli" did not complete successfully: exit code: 255

La variante amd64 se construit normalement. Pour la variante arm64, BuildKit télécharge l'image python:3.14-slim arm64, puis tente d'y exécuter /bin/sh pour le RUN... un binaire ARM, que le processeur x86 ne sait pas exécuter. Avec QEMU installé, la même construction aurait réussi, mais chaque instruction RUN de la variante ARM aurait été émulée.

Compiler en croisé : signalements-export

Go sait compiler pour une autre plateforme avec deux variables, GOOS et GOARCH, sans rien installer. Il suffit d'organiser le Dockerfile pour que la compilation s'exécute sur la plateforme de construction :

# syntax=docker/dockerfile:1
# L'étape de construction tourne sur la plateforme de la machine qui construit...
FROM --platform=$BUILDPLATFORM golang:1.26 AS construction
# ... et compile pour la plateforme cible, fournie par BuildKit.
ARG TARGETOS TARGETARCH TARGETVARIANT
WORKDIR /src
COPY go.mod *.go ./
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH GOARM=${TARGETVARIANT#v} \
    go build -trimpath -ldflags="-s -w" -tags timetzdata -o /signalements-export . \
 && echo "export:x:65532:65532:signalements-export:/nonexistent:/sbin/nologin" > /passwd \
 && echo "export:x:65532:" > /group

FROM scratch
COPY --from=construction /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=construction /passwd /etc/passwd
COPY --from=construction /group /etc/group
COPY --from=construction /signalements-export /signalements-export
USER 65532:65532
ENTRYPOINT ["/signalements-export"]

Par rapport à la leçon 5, trois lignes changent :

  • FROM --platform=$BUILDPLATFORM : l'image golang:1.26 utilisée est celle de la machine qui construit (amd64), quelle que soit la cible ;
  • ARG TARGETOS TARGETARCH TARGETVARIANT : on déclare les arguments prédéfinis dont on a besoin ;
  • GOOS=$TARGETOS GOARCH=$TARGETARCH GOARM=${TARGETVARIANT#v} : le compilateur produit un binaire pour la cible. GOARM précise la génération des processeurs ARM 32 bits ; ${TARGETVARIANT#v} retire le v de v7 (et vaut une chaîne vide pour les autres cibles, que Go ignore).

L'étape finale, scratch, ne contient aucune instruction RUN : rien n'y est exécuté, la plateforme cible n'y pose aucun problème. Les certificats copiés depuis l'étape de construction (amd64) sont des fichiers texte, valables pour toutes les architectures.

Construisons pour quatre plateformes d'un coup, et poussons dans un registre local :

$ /usr/bin/time -f "durée : %e s" docker buildx build --progress=plain \
    --platform linux/amd64,linux/arm64,linux/arm/v7,linux/riscv64 \
    -t 127.0.0.1:5000/export:1.0 --push .
...
#10 [linux/amd64->arm/v7 construction 4/4] RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 ...
#11 [linux/amd64 construction 4/4] RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GOARM= ...
#12 [linux/amd64->riscv64 construction 4/4] RUN CGO_ENABLED=0 GOOS=linux GOARCH=riscv64 GOARM= ...
#13 [linux/amd64->arm64 construction 4/4] RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GOARM= ...
...
durée : 15.57 s

(Sortie abrégée.) La notation linux/amd64->arm64 dit tout : l'étape tourne sur amd64 pour produire de l'arm64. Les quatre compilations s'exécutent en parallèle, nativement : 15,6 secondes pour quatre architectures. Le registre reçoit un index :

$ docker buildx imagetools inspect 127.0.0.1:5000/export:1.0
Name:      127.0.0.1:5000/export:1.0
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:c488aa236c0e23f66a91cab8559a6f11d1a84cd70a52df88ff68142c5f2b5594

Manifests:
  Name:        127.0.0.1:5000/export:1.0@sha256:c4a71f7f47daa8fc56dc185d8fd070f741f636273aa67cdc2b283f4b0bec6b8f
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    linux/amd64

  Name:        127.0.0.1:5000/export:1.0@sha256:d6c68aacc28c4390ef1439a22e6c724336940266f12d30e64bc0a0294a6806a1
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    linux/arm64

  Name:        127.0.0.1:5000/export:1.0@sha256:06707d8ca487bdfc69299cd5c1cce922837655d04b05f15beed264acc475fc56
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    linux/arm/v7

  Name:        127.0.0.1:5000/export:1.0@sha256:7d10806845e42c37565388b4ecadd498d75dd99ff152220df30c57ca6406b68c
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    linux/riscv64

  Name:        127.0.0.1:5000/export:1.0@sha256:bb30431eb30de0088b9243095cdb7e3c54938056bc968a17d220bf1a7da6f470
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    unknown/unknown
  Annotations:
    vnd.docker.reference.digest: sha256:c4a71f7f47daa8fc56dc185d8fd070f741f636273aa67cdc2b283f4b0bec6b8f
    vnd.docker.reference.type:   attestation-manifest
...

Quatre manifestes d'image et, pour chacun, un manifeste d'attestation (unknown/unknown) qui le désigne par son empreinte (leçon 8). Vu par Docker :

$ docker image ls --tree 127.0.0.1:5000/export:1.0
IMAGE                        ID             DISK USAGE   CONTENT SIZE   EXTRA
127.0.0.1:5000/export:1.0    c488aa236c0e       37.1MB           11MB
├─ linux/amd64               c4a71f7f47da       9.71MB         2.86MB
├─ linux/arm64               d6c68aacc28c       9.05MB         2.62MB
├─ linux/arm/v7              06707d8ca487       9.48MB         2.78MB
└─ linux/riscv64             7d10806845e4       8.85MB         2.68MB

Vérifier ce que l'on publie

La variante native s'exécute :

$ docker run --rm 127.0.0.1:5000/export:1.0 -version
signalements-export dev (linux/amd64, go1.26.8)

Les autres ne peuvent pas s'exécuter sur cette machine :

$ docker run --rm --platform linux/arm64 127.0.0.1:5000/export:1.0 -version
exec /signalements-export: exec format error

C'est attendu, et c'est une limite importante à garder en tête : sans machine de la bonne architecture (ou émulation), on ne teste pas réellement une variante. On peut au moins vérifier ce qu'elle contient, en extrayant les binaires :

$ for p in arm64 arm/v7 riscv64; do
    id=$(docker create --platform linux/$p 127.0.0.1:5000/export:1.0)
    docker cp $id:/signalements-export ./se-$(echo $p | tr / -) && docker rm $id
  done
$ file se-*
se-arm64:   ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, Go BuildID=spI23vjgN6...
se-arm-v7:  ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, Go BuildID=v4zNIwOg1lNr...
se-riscv64: ELF 64-bit LSB executable, UCB RISC-V, double-float ABI, version 1 (SYSV), statically linked, Go B...

Chaque binaire est bien de la bonne architecture, et statique. Le test réel se fait sur une machine de chaque architecture, en CI (leçon 10).

Python pour arm64, sans exécuter d'ARM

Python est un langage interprété : il n'y a rien à « compiler en croisé ». Les seules parties dépendantes de l'architecture sont les modules compilés des dépendances, comme psycopg_binary. Or pip sait télécharger les roues d'une autre plateforme sans les exécuter, à condition de tout prendre sous forme de roues précompilées. D'où ce Dockerfile, qui reprend la version psycopg[binary] de Signalements :

# syntax=docker/dockerfile:1

# Étape exécutée sur la machine qui construit : elle télécharge les roues de la plateforme CIBLE
FROM --platform=$BUILDPLATFORM python:3.14-slim AS dependances
ARG TARGETARCH
ENV PIP_DISABLE_PIP_VERSION_CHECK=1 PIP_ROOT_USER_ACTION=ignore
COPY requirements.txt .
RUN case "$TARGETARCH" in \
      amd64) arch=x86_64 ;; \
      arm64) arch=aarch64 ;; \
      *) echo "architecture non prise en charge : $TARGETARCH" >&2; exit 1 ;; \
    esac \
 && pip install --no-cache-dir --target /opt/site-packages \
      --only-binary=:all: --implementation cp --python-version 3.14 \
      --platform manylinux_2_28_$arch --platform manylinux2014_$arch \
      -r requirements.txt

# Étape finale, dans la plateforme cible : aucune instruction RUN, donc aucune émulation
FROM python:3.14-slim
COPY --from=dependances /opt/site-packages /opt/site-packages
WORKDIR /app
COPY app.py .
ENV PYTHONPATH=/opt/site-packages PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
USER 10001:10001
EXPOSE 8000
CMD ["python", "-m", "gunicorn", "--bind", "0.0.0.0:8000", "--workers", "2", "--no-control-socket", "app:app"]

Ce qui change par rapport aux leçons précédentes :

  • L'étape dependances tourne sur la plateforme de construction. Elle traduit l'architecture Docker (arm64) en architecture des étiquettes de roues Python (aarch64), puis demande à pip les roues de la cible : --platform donne les étiquettes acceptées (les roues Linux sont étiquetées manylinux, avec la version minimale de glibc requise : manylinux_2_28 exige glibc 2.28, que Debian 13 dépasse largement), --python-version et --implementation cp désignent CPython 3.14, et --only-binary=:all: interdit toute compilation depuis les sources, qui exécuterait du code pour la mauvaise architecture.
  • --target /opt/site-packages installe les paquets dans un simple répertoire, pas dans un environnement virtuel (dont les liens vers l'interpréteur seraient ceux de la machine de construction). PYTHONPATH le rend visible à l'interpréteur de l'image finale.
  • L'étape finale n'a aucun RUN : pas d'apt-get, pas de useradd. L'utilisateur est donné en numérique (USER 10001:10001), ce qui ne demande pas d'entrée dans /etc/passwd. Gunicorn est lancé comme module (python -m gunicorn), puisque --target n'installe pas de script exécutable utilisable directement.
$ /usr/bin/time -f "durée : %e s" docker buildx build --progress=plain --platform linux/amd64,linux/arm64 \
    -f Dockerfile.multi -t 127.0.0.1:5000/signalements:multi --push .
...
#11 2.462 Downloading psycopg_binary-3.3.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl (5.2 MB)
...
#12 2.369 Downloading psycopg_binary-3.3.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl (6.8 MB)
...
durée : 8.18 s
$ docker image ls --tree 127.0.0.1:5000/signalements:multi
IMAGE                                ID             DISK USAGE   CONTENT SIZE   EXTRA
127.0.0.1:5000/signalements:multi    70a1c5e894b9        106MB          106MB
├─ linux/amd64                       61db135dac28       51.8MB         51.8MB
└─ linux/arm64                       1ca1fdf51cc2       53.8MB         53.8MB

Deux téléchargements de psycopg_binary, l'un pour x86_64, l'autre pour aarch64, dans deux étapes parallèles : 8 secondes pour les deux architectures. La variante amd64 fonctionne contre PostgreSQL :

$ docker run -d --name app --network signalements -p 127.0.0.1:8000:8000 \
    -e DATABASE_URL=postgresql://postgres:essai@db:5432/postgres 127.0.0.1:5000/signalements:multi
$ curl -s -X POST localhost:8000/signalements -H 'Content-Type: application/json' \
    -d '{"lieu":"Quai des Arts","description":"Garde-corps descellé"}'
{"description":"Garde-corps descellé","id":1,"lieu":"Quai des Arts"}
$ docker exec app python -c "import platform, psycopg; print(platform.machine(), psycopg.pq.__impl__)"
x86_64 binary

Et la variante arm64 contient bien un module compilé pour ARM :

$ id=$(docker create --platform linux/arm64 127.0.0.1:5000/signalements:multi)
$ docker cp $id:/opt/site-packages/psycopg_binary ./pb-arm64 && docker rm $id
$ file pb-arm64/pq.cpython-314-*.so
pb-arm64/pq.cpython-314-aarch64-linux-gnu.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically l...
$ docker run --rm --platform linux/arm64 127.0.0.1:5000/signalements:multi python -V
exec /usr/local/bin/python: exec format error

Comme pour Go, la variante ARM n'a pas pu être exécutée sur la machine de test, qui n'a ni processeur ARM ni émulation ; son contenu a été vérifié, son fonctionnement devra l'être sur une machine ARM.

Note

Cette technique a deux conditions : toutes les dépendances doivent publier des roues pour chaque plateforme cible (c'est le cas des bibliothèques courantes), et l'étape finale ne doit rien exécuter. Pour psycopg[c], qui se compile contre libpq (leçon 2), elle ne s'applique pas : il faut alors une chaîne de compilation croisée, de l'émulation ou un nœud natif.

Sous le capot

Comment BuildKit construit plusieurs plateformes. Il résout le Dockerfile une fois par plateforme cible, ce qui produit autant de graphes LLB, exécutés en parallèle. Les étapes déclarées avec FROM --platform=$BUILDPLATFORM sont identiques dans tous les graphes jusqu'au premier argument qui dépend de la cible : elles ne sont exécutées qu'une fois, puis partagées par le cache. À la fin, l'exporteur crée un manifeste par plateforme et un index qui les référence.

Comment le client choisit sa variante. docker pull et containerd lisent l'index, puis comparent la plateforme de chaque manifeste à celle de la machine, en tenant compte des variantes compatibles : une machine arm64 accepte une image arm/v7 s'il n'y a pas mieux, une machine amd64/v3 accepte amd64. L'option --platform force un choix ; c'est ainsi que docker create --platform linux/arm64 a récupéré la variante ARM sur une machine x86.

Ce que fait l'émulation. L'image tonistiigi/binfmt (ou les paquets QEMU des distributions) enregistre dans /proc/sys/fs/binfmt_misc des interpréteurs QEMU pour chaque architecture : quand le noyau doit exécuter un binaire ARM, il lance à sa place qemu-aarch64 avec ce binaire. C'est transparent pour Docker, mais chaque instruction est traduite : une compilation C ou une installation de paquets volumineuse peut prendre dix fois plus de temps. Cet enregistrement modifie la configuration du noyau de l'hôte (et demande des privilèges) ; il n'a pas été fait sur la machine de test de ce cours.

Pièges courants

exec format error pendant la construction. Une instruction RUN s'exécute dans une étape de la plateforme cible, sans émulation disponible. Soit l'étape doit tourner sur $BUILDPLATFORM (compilation croisée), soit l'instruction doit disparaître de l'étape finale, soit il faut de l'émulation ou un nœud natif.

exec format error à l'exécution. L'index ne contient pas votre plateforme (vérifiez avec docker buildx imagetools inspect), ou l'image a été construite et poussée depuis un Mac ARM sans --platform : elle ne contient que la variante arm64, et les serveurs amd64 ne peuvent pas l'exécuter. Fixez explicitement --platform en CI.

Oublier --platform=$BUILDPLATFORM sur l'étape de compilation. Le Dockerfile fonctionne quand même si QEMU est installé... mais la compilation Go de la variante ARM est alors émulée au lieu d'être croisée, et prend plusieurs minutes au lieu de quelques secondes. Une construction multi-plateformes anormalement lente révèle souvent cet oubli.

Des arguments de plateforme vides. TARGETARCH et ses voisins sont des arguments prédéfinis, mais une étape ne les voit que si elle les déclare (ARG TARGETARCH). Sans déclaration, la variable est vide et GOARCH= compile pour l'architecture de construction, en silence.

Une variante cassée et non testée. Une image arm64 qui se construit n'est pas une image arm64 qui fonctionne : une dépendance peut manquer de roue pour une plateforme (pip échoue alors avec No matching distribution found, grâce à --only-binary), ou se comporter différemment. Testez chaque variante sur une machine de la bonne architecture.

docker build --load d'une image multi-plateformes. Avec l'ancien stockage d'images de Docker, --load ne savait charger qu'une seule plateforme. Le magasin containerd, par défaut depuis Docker 29, sait stocker l'index complet (c'est ce que montre docker image ls --tree), mais seule la variante native est exécutable.

Sécurité

  • Chaque variante est une image à part. Elle a ses propres couches, ses propres paquets, donc ses propres vulnérabilités : une analyse Trivy doit porter sur chaque plateforme (--platform de Trivy), et une signature (leçon 9) porte sur l'index ou sur chaque manifeste, selon l'outil.
  • Les attestations par plateforme. Les manifestes d'attestation de l'index désignent chacun un manifeste de plateforme ; un consommateur peut ainsi vérifier la provenance de la variante qu'il exécute réellement (leçon 8).
  • L'émulation élargit la surface. Enregistrer QEMU dans binfmt_misc fait exécuter par l'émulateur tout binaire étranger qui se présente sur l'hôte, pas seulement pendant les constructions. Sur une machine de CI partagée, c'est un composant de plus à maintenir à jour. Préférez la compilation croisée.

En production

  • Construisez les variantes dont vous avez besoin, pas plus. linux/amd64 et linux/arm64 couvrent les serveurs, les postes Apple et les instances ARM des clouds. Chaque plateforme ajoutée allonge la construction, l'analyse et les tests.
  • Sur Kapsule, les groupes de nœuds (pools) peuvent être de types différents, dont des instances ARM. Un cluster mixte ne fonctionne que si toutes les images déployées existent pour les deux architectures, y compris les images tierces (contrôleurs, agents de supervision). Vérifiez-le avant d'ajouter un groupe de nœuds ARM, et utilisez les étiquettes de nœuds (kubernetes.io/arch) pour placer les charges.
  • En CI, l'action docker/setup-qemu-action installe l'émulation et docker/setup-buildx-action crée le builder ; mais si votre Dockerfile compile en croisé, vous n'avez pas besoin de la première. Pour des constructions lourdes qui exigent l'exécution native, GitHub propose des agents ARM, et BuildKit peut agréger des nœuds de plusieurs architectures dans un seul builder (docker buildx create --append). La leçon 10 en donne un exemple.
  • Testez sur la vraie architecture, au moins un test de fumée par variante, sur un agent ARM ou une instance ARM éphémère.

Exercices

1. Lire un index. Avec docker buildx imagetools inspect, listez les plateformes publiées par postgres:18.6, nginx:1.29 et gcr.io/distroless/static-debian13:nonroot. Lesquelles des plateformes de postgres:18.6 ne sont pas couvertes par distroless/static ?

Solution
$ for i in postgres:18.6 nginx:1.29 gcr.io/distroless/static-debian13:nonroot; do
    echo "== $i"; docker buildx imagetools inspect $i --raw | jq -r '.manifests[] | select(.platform.os != "unknown") | "\(.platform.os)/\(.platform.architecture)\(if .platform.variant then "/"+.platform.variant else "" end)"'
  done

Au 1er octobre 2026 :

== postgres:18.6
linux/amd64
linux/arm/v5
linux/arm/v7
linux/arm64/v8
linux/386
linux/ppc64le
linux/riscv64
linux/s390x
== nginx:1.29
linux/amd64
linux/arm/v5
linux/arm/v7
linux/arm64/v8
linux/386
linux/ppc64le
linux/riscv64
linux/s390x
== gcr.io/distroless/static-debian13:nonroot
linux/amd64
linux/arm64/v8
linux/arm/v7
linux/s390x
linux/ppc64le
linux/riscv64

Les images officielles Docker couvrent huit plateformes ; distroless n'en publie que six, sans armv5 ni 386. Une image construite sur distroless/static ne peut donc pas être publiée pour ces deux plateformes. Le réflexe à retenir : avant de déployer sur une architecture, vérifiez que toutes les images nécessaires, base comprise, y sont publiées.

2. Trouver l'oubli. Ce Dockerfile produit une image multi-plateformes dont la variante arm64 affiche exec format error au démarrage, alors que la construction a réussi sans QEMU. Trouvez l'erreur.

FROM --platform=$BUILDPLATFORM golang:1.26 AS construction
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH go build -o /outil .

FROM scratch
COPY --from=construction /outil /outil
ENTRYPOINT ["/outil"]
Solution

L'étape construction ne déclare pas ARG TARGETARCH : la variable est vide, GOARCH= vaut la valeur par défaut, celle de la machine de construction (amd64). La variante arm64 contient donc un binaire amd64, ce que confirme l'extraction du résultat :

$ docker buildx build -q --platform linux/arm64 --output type=local,dest=sortie .
$ file sortie/outil
sortie/outil: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ...

Ajoutez ARG TARGETARCH (et TARGETOS) après le FROM, et vérifiez chaque binaire avec file, comme dans la leçon.

3. Ajouter une plateforme Python (niveau 200). Étendez le Dockerfile Python de la leçon à linux/arm/v7. Que se passe-t-il ? Pourquoi ?

Solution

Il faut ajouter le cas arm dans le case (étiquette de roue armv7l). La construction échoue alors, et pas forcément là où on l'attend :

#11 2.048 ERROR: Could not find a version that satisfies the requirement markupsafe>=2.1.1 (from flask) (from versions: none)
#11 2.048 ERROR: No matching distribution found for markupsafe>=2.1.1

C'est markupsafe, une dépendance transitive de Flask avec une petite extension C, qui ne publie pas de roue manylinux pour ARM 32 bits (et psycopg-binary non plus) : --only-binary=:all: interdit de compiler, ce qui est le comportement voulu, plutôt que de produire une image cassée. Pour cette plateforme, il faudrait compiler les dépendances (émulation ou nœud natif) ou se passer de la version binaire.

4. Choisir une stratégie (niveau 300). Pour chacun de ces projets, choisissez entre compilation croisée, émulation et nœuds natifs, et justifiez : (a) un service Rust sans dépendance C ; (b) une application Node.js qui dépend d'une bibliothèque native compilée à l'installation (node-gyp) ; (c) une image d'outillage qui installe 300 paquets Debian, à publier pour amd64 et arm64 chaque nuit.

Solution

(a) Compilation croisée : Rust cible facilement aarch64-unknown-linux-musl ou -gnu depuis amd64 avec la bonne chaîne d'outils, et l'étape finale ne fait que copier un binaire. (b) Émulation si la compilation reste courte, sinon nœuds natifs ; la compilation croisée de modules node-gyp est possible mais délicate. (c) Nœuds natifs, ou émulation si la durée est acceptable la nuit : apt-get install n'a pas d'équivalent en compilation croisée (les scripts d'installation des paquets s'exécutent), et 300 paquets sous émulation peuvent prendre longtemps. Une instance ARM éphémère chez le fournisseur de cloud, ajoutée comme nœud au builder, règle la question.

Récapitulatif

  • Une image multi-architectures est un index de manifestes, un par plateforme ; le client choisit le sien.
  • BUILDPLATFORM est la machine qui construit, TARGETPLATFORM l'image produite ; une étape tourne par défaut sur la cible, d'où exec format error sans émulation.
  • Compilez en croisé : FROM --platform=$BUILDPLATFORM pour l'étape de construction, ARG TARGETOS TARGETARCH déclarés, et une étape finale sans RUN. Go : quatre architectures en 15 secondes, sans émulation.
  • Pour Python, pip peut télécharger les roues de la cible (--platform, --only-binary=:all:, --target) : image amd64 + arm64 en 8 secondes, tant que toutes les dépendances publient des roues.
  • QEMU en dernier recours, nœuds natifs pour les constructions lourdes ; et testez chaque variante sur sa vraie architecture.

Pour aller plus loin

  • La page Multi-platform builds de la documentation Docker, qui détaille les trois stratégies et la configuration des builders à plusieurs nœuds.
  • La PEP 600, qui explique les étiquettes manylinux et la compatibilité des roues Linux.
  • Leçon suivante : SBOM et attestations de provenance, pour dire précisément ce que contient chaque variante et comment elle a été construite.
Voir ma constellation →

Sources