25 - Builds multi-platform
Ce que tu vas apprendre
- La différence entre ARM et x86 (et pourquoi Apple Silicon a change la donne)
- Configurer docker buildx pour le multi-platform
- Builder une image pour plusieurs architectures en une commande
- Comment fonctionne l'emulation QEMU
- Les performances natives vs emulees
- Mettre en place des builds multi-platform en CI
Prerequisites
Avoir suivi l'article sur l'optimisation. Connaitre docker buildx. Avoir Docker Desktop ou Docker Engine recent.
Le jour ou j'ai achete un Mac M1, j'ai découvert un problème que je n'avais jamais eu : mes images Docker ne marchaient plus sur le serveur de prod. Mes images etaient buildees pour ARM (le processeur du Mac), mon serveur tourne sur x86. Le conteneur demarrait et crashait immédiatement avec un "exec format error". Bienvenue dans le monde du multi-platform.
ARM vs x86 : le contexte
Pendant 20 ans, le monde des serveurs etait simple : tout etait x86_64 (aussi appele amd64). Intel et AMD, meme jeu d'instructions, tout est compatible.
Puis Apple a sorti ses puces M1 (ARM), AWS a lance Graviton (ARM), et soudainement les développeurs buildent sur une architecture et deploient sur une autre. Ou sur les deux.
Les architectures courantes dans Docker :
linux/amd64: Intel/AMD, la majorite des serveurs cloudlinux/arm64: Apple Silicon, AWS Graviton, Raspberry Pi 4linux/arm/v7: Raspberry Pi 3, anciens appareils ARM
Quand tu fais docker pull nginx, Docker telecharge automatiquement la version qui correspond a ton architecture. Mais si l'image n'existe pas pour ton archi, ca ne marche pas.
docker buildx : le builder multi-platform
buildx est le builder etendu de Docker. Il remplace docker build et ajoute le support multi-platform :
bash# Verifier les builders disponibles
docker buildx ls
# Creer un builder multi-platform
docker buildx create --name multibuilder --use
# Verifier les plateformes supportees
docker buildx inspect --bootstrap
Le builder par défaut ne supporte que ta plateforme native. Le builder multibuilder utilise QEMU pour emuler les autres architectures.
Builder pour plusieurs plateformes
bash# Builder pour ARM et x86 en une commande
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t monregistry/monapp:latest \
--push .
Le --push est nécessaire parce que les images multi-platform ne peuvent pas etre stockees localement (c'est une liste de manifestes qui pointe vers plusieurs images). Il faut les pousser vers un registry.
Pour tester localement sans registry :
bash# Builder pour une seule plateforme et charger localement
docker buildx build \
--platform linux/amd64 \
--load \
-t monapp:latest .
# Ou exporter en fichier
docker buildx build \
--platform linux/amd64,linux/arm64 \
--output type=oci,dest=monapp.tar .
Le flag --platform dans le Dockerfile
Tu peux aussi spécifier la plateforme dans le Dockerfile pour les cas ou tu as besoin de mixer :
dockerfile# Le builder tourne sur ta plateforme native
FROM --platform=$BUILDPLATFORM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# L'image finale cible la plateforme demandee
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
Le $BUILDPLATFORM correspond a ta machine. Le stage builder tourne nativement (rapide), et seule l'image finale est construite pour la plateforme cible. Ca fonctionne bien pour les builds JavaScript ou le code est interprète, pas compile.
Pour les langages compiles (Go, Rust), il faut cross-compiler :
dockerfileFROM --platform=$BUILDPLATFORM golang:1.22-alpine AS builder
ARG TARGETOS TARGETARCH
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o server .
FROM alpine:3.19
COPY --from=builder /app/server /server
CMD ["/server"]
Les variables $TARGETOS et $TARGETARCH sont injectees automatiquement par BuildKit. Go sait cross-compiler nativement, donc le build est rapide meme pour une autre architecture.
QEMU : l'emulation transparente
Quand tu builds une image ARM sur une machine x86 (ou inversement), Docker utilise QEMU pour emuler le processeur cible. C'est transparent : tu ne configures rien, les binaires QEMU sont enregistres dans le kernel.
bash# Installer les emulateurs QEMU (Docker Desktop les a deja)
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
# Verifier les architectures supportees
docker buildx inspect --bootstrap
Sur paltemps.fr, je build sur un Mac M1 (ARM) et je deploie sur un VPS x86. L'emulation QEMU me permet de tester l'image x86 localement avant de déployer.
Performances : natif vs emule
Le problème de l'emulation, c'est la vitesse. Un build emule est 5 a 20 fois plus lent qu'un build natif :
| Opération | Natif | Emule | Ralentissement |
|---|---|---|---|
| npm ci (500 deps) | 30s | 4min | x8 |
| go build | 15s | 2min | x8 |
| cargo build (Rust) | 2min | 30min+ | x15 |
| Python pip install | 20s | 3min | x9 |
Pour les langages interprètes (Node.js, Python), c'est tolerable. Pour Rust, c'est inutilisable en emulation. La solution : cross-compiler quand le langage le permet, ou builder nativement sur chaque architecture.
Pour le dev quotidien, builde pour ta plateforme locale. Le multi-platform, c'est pour la CI et les releases.
CI multi-platform
Un workflow GitHub Actions qui build pour ARM et x86 :
yamlname: Build multi-platform
on:
push:
tags: ["v*"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
cache-from: type=gha
cache-to: type=gha,mode=max
Le setup-qemu-action installe les emulateurs. Le setup-buildx-action configure le builder. Le cache GitHub Actions (type=gha) est gratuit et accéléré les builds suivants, comme on l'a vu dans l'article sur l'optimisation.
Pour aller plus vite en CI, certains utilisent des runners natifs pour chaque architecture :
yamljobs:
build-amd64:
runs-on: ubuntu-latest
# build natif x86
build-arm64:
runs-on: ubuntu-latest-arm64 # runner ARM natif
# build natif ARM
manifest:
needs: [build-amd64, build-arm64]
runs-on: ubuntu-latest
steps:
- run: |
docker manifest create ghcr.io/myapp:latest \
ghcr.io/myapp:latest-amd64 \
ghcr.io/myapp:latest-arm64
docker manifest push ghcr.io/myapp:latest
Chaque architecture build nativement (rapide), puis un job final combine les deux dans un manifest. C'est plus complexe a maintenir mais nettement plus rapide pour les gros projets.
Manifest lists
Quand tu fais docker pull nginx, Docker recoit en fait une manifest list qui pointe vers plusieurs images :
bash# Voir le manifeste d'une image
docker manifest inspect nginx:alpine
# Le resultat montre les plateformes disponibles
# linux/amd64, linux/arm64, linux/arm/v7, etc.
Docker choisit automatiquement la bonne image pour ta plateforme. C'est pour ca que docker pull fonctionne partout sans spécifier l'architecture.
Résumé
- Apple Silicon (ARM) et les serveurs x86 ont créé le besoin du multi-platform.
docker buildx build --platform linux/amd64,linux/arm64créé des images pour les deux.- QEMU emule les architectures manquantes, mais c'est 5 a 20x plus lent.
$BUILDPLATFORMet$TARGETARCHpermettent de cross-compiler dans le Dockerfile.- En CI, les GitHub Actions officielles Docker simplifient tout le setup.
- Pour le dev, reste sur ta plateforme native. Le multi-platform c'est pour la release.
Article précédent : Docker 24 - Optimisation Article suivant : Docker 26 - Ressources