Build multi-étapes Docker : réduire les images Go/Java/Rust de GB à MB

Une image de 650 Mo qui m’a fait réfléchir
Vendredi après-midi, je regardais un Pod sur le dashboard K8s qui tournait depuis cinq minutes à tirer l’image, agacé. Une simple application Spring Boot, empaquetée en image Docker : 650 Mo. Un stagiaire m’a demandé : « Pourquoi c’est si gros ? » J’ai hésité — je n’avais jamais vraiment creusé la question.
Ce soir-là, j’ai étudié le sujet. Le lendemain matin, la même application : 89 Mo. Déploiement de 5 minutes à moins d’1 minute. Le regard du stagiaire avait changé.
En fait, quelques lignes modifiées dans le Dockerfile. La technique s’appelle le build multi-étapes.
Au début, je pensais que c’était inutile. Nos images Go faisaient 295 Mo, Java dépassait souvent 500 Mo — on s’était habitués. Puis j’ai vu quelqu’un compresser une image Rust de 2 Go à 11 Mo. Là, j’ai compris qu’on empaquetait peut-être mal.
Le cœur du problème : les langages compilés ont besoin de compilateurs et d’outils de build, mais le runtime n’en a pas besoin. Les Dockerfile traditionnels embarquent Maven, Gradle, le compilateur Go — comme emmener perceuse et ciment lors d’un déménagement.
Aujourd’hui, trois cas réels en Go, Java et Rust :
- Go : de 295 Mo à 6,47 Mo (-98 %)
- Java Spring Boot : de 650 Mo à 89 Mo (-86 %)
- Rust : de 2 Go à 11,2 Mo (-99,4 %)
Si vous ne vous intéressez qu’à un langage, sautez au chapitre correspondant. Chaque cas est complet et copiable.
Pourquoi vos images sont-elles si volumineuses ?
Tableau comparatif de mes tests :
| Langage | Build mono-étape | Build multi-étapes | Réduction |
|---|---|---|---|
| Go | 295 Mo | 6,47 Mo | 98 % |
| Java Spring Boot | 650 Mo | 89 Mo | 86 % |
| Rust | 2,1 Go | 11,2 Mo | 99,4 % |
La première fois, j’étais sidéré. Surtout Rust : 2 Go à 11 Mo, ce n’est pas une optimisation, c’est de la magie.
En analysant la composition des images, j’ai compris. Les langages compilés ont besoin d’un compilateur :
- Compilateur Go : ~300 Mo
- Maven + OpenJDK : ~500 Mo
- Toolchain Rust : ~1,5 Go
Un Dockerfile traditionnel ressemble à ça :
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o myapp
CMD ["./myapp"]
Rien d’anormal en apparence. En réalité, gros problème : l’image golang:1.21 (295 Mo) sert de base, avec compilateur, outils de build, outils de debug. Après compilation, tout reste dans l’image finale.
Comme acheter un appartement brut, faire des travaux, puis laisser ciment, perceuse, scie et la caisse à outils du maçon dans la pièce avant d’emménager. Absurde — et pourtant beaucoup de Dockerfile font pareil.
Au runtime, il ne faut que le binaire compilé. Go produit souvent quelques Mo à quelques dizaines de Mo. Java un jar un peu plus gros. Rust aussi. Le reste, ce sont des outils de build et la surcharge de l’image de base.
Cette lourdeur ne gaspille pas seulement du stockage :
- Pull lent : en CI/CD, chaque déploiement tire l’image. 650 Mo sur un réseau lent, c’est interminable
- Risque de sécurité : compilateurs, code source, outils de build en production — autant d’outils pour un attaquant
- Cache gaspillé : une ligne de code modifiée, toute l’image se reconstruit car compilateur et code sont couplés
Une fois sur Alibaba Cloud, cinq personnes déployaient en même time, chacune avec 500 Mo+ d’image — le réseau interne saturé. J’ai alors décidé d’étudier l’optimisation.
Build multi-étapes : un Dockerfile, deux environnements
Le build multi-étapes est une fonctionnalité de Docker 17.05. Idée simple : plusieurs phases dans un Dockerfile — les premières compilent, les suivantes exécutent, seuls les artefacts nécessaires passent d’une phase à l’autre.
En clair : l’équipe rénove en phase 1, puis vous n’emmenez que la maison finie en phase 2 — le ciment et la perceuse restent en phase 1.
Exemple minimal :
# Phase 1 : build
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
# Phase 2 : runtime
FROM alpine:3.18
WORKDIR /app
COPY --from=builder /app/myapp .
CMD ["./myapp"]
Points clés :
- Premier FROM suivi de
AS builder— nom de la phase - Deuxième FROM — nouvelle phase avec alpine, plus léger
- COPY —from=builder — copie le binaire, le reste est jeté
La phase golang:1.21 fait 295 Mo, mais l’image finale = alpine (~5 Mo) + binaire (quelques Mo), soit une dizaine de Mo.
Quand j’ai compris, ça m’a frappé : « seulement le résultat, pas le processus ». Compilateur, code source, fichiers intermédiaires = processus. Le binaire = résultat. Docker jette la phase 1, ne garde que la phase 2.
Pour déboguer la phase 1 :
docker build --target builder -t myapp:debug .
--target builder construit jusqu’à la phase builder sans exécuter la phase 2.
Vous pouvez avoir trois, quatre phases ou plus :
FROM node:18 AS frontend-builder
# Build frontend
FROM golang:1.21 AS backend-builder
# Build backend
FROM nginx:alpine
# Copier les artefacts frontend et backend
COPY --from=frontend-builder /app/dist /usr/share/nginx/html
COPY --from=backend-builder /app/api /usr/local/bin/api
Chaque phase a sa tâche, tout converge vers le runtime. Le Dockerfile devient très lisible.
Aujourd’hui, pour tout langage compilé, j’utilise le build multi-étapes par défaut. C’est devenu un réflexe.
Alléger au maximum une application Go
Go est mon langage préféré pour l’optimisation d’images. Le binaire Go est statiquement lié, sans dépendance système — il tourne dans une image vide.
Mauvaise pratique (ne pas imiter) :
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o myapp
CMD ["./myapp"]
Build :
docker build -t myapp:old .
docker images myapp:old
# REPOSITORY TAG IMAGE ID SIZE
# myapp old abc123def456 295MB
295 Mo pour un simple service HTTP.
Version multi-étapes optimisée :
# Phase build
FROM golang:1.21-alpine AS builder
WORKDIR /app
# Copier les dépendances et les télécharger (cache)
COPY go.mod go.sum ./
RUN go mod download
# Copier le code source et compiler
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o myapp .
# Phase runtime
FROM scratch
WORKDIR /app
COPY --from=builder /app/myapp .
EXPOSE 8080
CMD ["./myapp"]
Nouveau build :
docker build -t myapp:new .
docker images myapp:new
# REPOSITORY TAG IMAGE ID SIZE
# myapp new def456ghi789 6.47MB
6,47 Mo ! De 295 Mo, -98 %.
Points clés :
1. CGO_ENABLED=0
Désactive CGO, binaire statique pur. Si vous utilisez des bibliothèques C (ex. SQLite), il faut une autre image de base.
2. -ldflags=“-w -s”
Optimisation compilateur :
-w: supprime les infos de debug-s: supprime la table des symboles
Réduction de 20 à 30 % supplémentaire. En production, ces infos sont rarement utiles.
3. FROM scratch
scratch est l’image vide Docker, 0 octet. Un binaire Go statique tourne directement dedans.
4. Cache des dépendances
Copier d’abord go.mod et go.sum, puis go mod download. Tant que ces fichiers ne changent pas, Docker réutilise le cache. Modifier le code métier ne relance pas le téléchargement des dépendances.
Avec cette astuce, rebuild après modification de code : de 2 minutes à 15 secondes.
Avancé : fuseau horaire et certificats CA
scratch n’a ni fuseaux horaires ni certificats CA. Pour HTTPS ou gestion de timezone, copiez depuis builder :
FROM scratch
WORKDIR /app
# Données fuseau horaire
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
# Certificats CA
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/myapp .
ENV TZ=Asia/Shanghai
CMD ["./myapp"]
Ou utilisez gcr.io/distroless/static-debian11 (~2 Mo, avec timezone et CA) :
FROM gcr.io/distroless/static-debian11
COPY --from=builder /app/myapp /app/myapp
CMD ["/app/myapp"]
Je préfère distroless — plus simple.
Alléger Java/Spring Boot proprement
Java est plus complexe que Go. Il faut un JRE, pas d’image scratch. Mais avec la bonne méthode, le volume baisse fortement.
Pratique traditionnelle (courante) :
FROM maven:3.8-openjdk-17
WORKDIR /app
COPY . .
RUN mvn clean package -DskipTests
CMD ["java", "-jar", "target/myapp.jar"]
Image finale : 650 Mo. Maven ~500 Mo + jar + caches.
Version multi-étapes :
# Phase build
FROM maven:3.8-openjdk-17-slim AS builder
WORKDIR /app
# Copier pom.xml, télécharger les dépendances (cache)
COPY pom.xml .
RUN mvn dependency:go-offline -B
# Copier le code source et packager
COPY src ./src
RUN mvn package -DskipTests
# Phase runtime
FROM openjdk:17-jre-slim
WORKDIR /app
# Copier uniquement le jar
COPY --from=builder /app/target/*.jar app.jar
# Optimisation JVM
ENV JAVA_OPTS="-Xms128m -Xmx512m -XX:+UseContainerSupport"
EXPOSE 8080
CMD java $JAVA_OPTS -jar app.jar
Résultat :
docker images myapp:new
# REPOSITORY TAG IMAGE ID SIZE
# myapp new xyz789abc012 89MB
De 650 Mo à 89 Mo, -86 %.
Points clés :
1. JDK vs JRE
Build avec openjdk-17 (compilateur), runtime avec openjdk-17-jre-slim (runtime seulement) :
- OpenJDK 17 : ~500 Mo
- OpenJDK 17 JRE : ~200 Mo
- OpenJDK 17 JRE Slim : ~80 Mo
2. Cache Maven
Copier d’abord pom.xml, exécuter mvn dependency:go-offline. Tant que pom.xml ne change pas, la couche est en cache. Modifier le code métier ne relance pas le téléchargement.
Crucial. Avant, chaque rebuild retéléchargeait les dépendances — dépôt Maven à l’étranger, parfois 15 minutes bloqué. Maintenant, quelques secondes.
3. Paramètres JVM
-XX:+UseContainerSupport : la JVM respecte les limites mémoire du conteneur. Sans ça, elle alloue selon la machine hôte — OOM fréquent.
-Xms128m -Xmx512m : plage heap. Ajustez selon votre application, pas 1 Go par défaut.
Version Gradle
Avec Gradle :
FROM gradle:8.5-jdk17 AS builder
WORKDIR /app
# Fichiers de config Gradle
COPY build.gradle settings.gradle ./
COPY gradle ./gradle
# Télécharger les dépendances
RUN gradle dependencies --no-daemon
# Code source et build
COPY src ./src
RUN gradle bootJar --no-daemon
FROM openjdk:17-jre-slim
WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar
ENV JAVA_OPTS="-Xms128m -Xmx512m -XX:+UseContainerSupport"
CMD java $JAVA_OPTS -jar app.jar
Piège : build en couches Spring Boot
Spring Boot 2.3+ supporte le découpage en couches du jar pour optimiser le cache :
FROM maven:3.8-openjdk-17-slim AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests
RUN java -Djarmode=layertools -jar target/*.jar extract
FROM openjdk:17-jre-slim
WORKDIR /app
# Ordre des couches — dépendances changent le moins
COPY --from=builder /app/dependencies/ ./
COPY --from=builder /app/spring-boot-loader/ ./
COPY --from=builder /app/snapshot-dependencies/ ./
COPY --from=builder /app/application/ ./
CMD ["java", "org.springframework.boot.loader.JarLauncher"]
Modifier le code métier n’invalide pas la couche dépendances — build plus rapide. Honnêtement, je n’utilise pas souvent cette variante ; la version simple suffit en général.
Déploiement minimal pour Rust
Rust offre l’optimisation la plus spectaculaire. Toolchain énorme (1,5 Go+), binaire final minuscule. Ma première fois : 2,1 Go à 11,2 Mo.
Pratique traditionnelle :
FROM rust:1.75
WORKDIR /app
COPY . .
RUN cargo build --release
CMD ["./target/release/myapp"]
Image : 2,1 Go. rustc, cargo, dépendances — tout embarqué.
Version multi-étapes :
# Phase build
FROM rust:1.75 AS builder
WORKDIR /app
# Copier les dépendances, compiler d'abord les deps (cache)
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm -rf src
# Copier le vrai code, compiler l'application
COPY src ./src
RUN touch src/main.rs # Mettre à jour le timestamp, forcer recompilation
RUN cargo build --release
# Phase runtime
FROM gcr.io/distroless/cc-debian11
WORKDIR /app
COPY --from=builder /app/target/release/myapp .
CMD ["./myapp"]
Résultat :
docker images myapp:new
# REPOSITORY TAG IMAGE ID SIZE
# myapp new rst345uvw678 11.2MB
11,2 Mo ! -99,4 %.
Cache spécifique Rust
La compilation des dépendances Rust est lente — souvent 15 minutes. L’astuce : créer un faux main.rs, laisser cargo compiler les deps, supprimer le faux fichier, copier le vrai code.
Tant que Cargo.toml et Cargo.lock ne changent pas, la couche deps est en cache. Modifier le code métier ne recompile que votre code.
Sur un projet moyen : rebuild de 15 minutes à 2 minutes.
Liaison statique vs dynamique
Par défaut, le binaire Rust peut dépendre de glibc. Code Rust pur sans bibliothèque C : compilez avec musl, image scratch :
FROM rust:1.75 AS builder
WORKDIR /app
# Installer la toolchain musl
RUN rustup target add x86_64-unknown-linux-musl
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release --target x86_64-unknown-linux-musl
RUN rm -rf src
COPY src ./src
RUN touch src/main.rs
RUN cargo build --release --target x86_64-unknown-linux-musl
# Phase runtime
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/myapp /myapp
CMD ["/myapp"]
Encore plus petit, peut-être 5-8 Mo.
En pratique, j’utilise gcr.io/distroless/cc — meilleure compatibilité, quelques Mo de plus, ça vaut le coup.
Piège : emplacement du cache Cargo
Certains articles conseillent de cacher /usr/local/cargo. À éviter. Beaucoup d’artefacts intermédiaires, couche de cache énorme, build plus lent. J’ai testé : couche de 800 Mo, attente interminable.
Techniques avancées pour un build rapide et stable
Au-delà du build multi-étapes de base :
1. Fichier .dockerignore
Essentiel et souvent ignoré. Comme .gitignore, il indique à Docker quels fichiers ne pas copier dans le contexte de build.
Sans ça, Docker envoie tout le projet (node_modules, .git, target…) au daemon. Projet de plusieurs centaines de Mo, 30 secondes juste pour envoyer le contexte.
.dockerignore sur chaque projet :
# Contrôle de version
.git
.gitignore
# Répertoires de dépendances
node_modules
target
dist
build
# IDE
.vscode
.idea
*.swp
# Tests et documentation
**/*_test.go
**/*_test.rs
*.md
docs/
# Variables d'environnement et clés
.env
.env.local
*.key
*.pem
Build 3 à 5 fois plus rapide, surtout en CI/CD.
2. Déboguer le build multi-étapes
--target en détail. Build échoue en phase builder ?
# Construire jusqu'à builder
docker build --target builder -t myapp:debug .
# Entrer dans l'image
docker run -it myapp:debug sh
# Exécuter manuellement les commandes de build
Très utile. Une fois, mon build Go échouait avec une erreur obscure. J’ai entré dans builder, lancé go build manuellement — incompatibilité de version Go et dépendances.
3. Nommer les phases
Des noms explicites, pas stage1/stage2 :
# Mauvais
FROM golang:1.21 AS stage1
FROM node:18 AS stage2
FROM nginx AS stage3
# Bon
FROM golang:1.21 AS backend-builder
FROM node:18 AS frontend-builder
FROM nginx AS runtime
Six mois plus tard, vous vous remercierez.
4. Build parallèle avec BuildKit
BuildKit : moteur de build nouvelle génération, parallélisme, meilleur cache :
export DOCKER_BUILDKIT=1
docker build .
Ou temporairement :
DOCKER_BUILDKIT=1 docker build .
Avantages :
- Phases indépendantes en parallèle
- Cache plus intelligent
- Sortie plus claire
Je l’utilise partout, y compris en CI/CD avec DOCKER_BUILDKIT=1.
5. Fixer la version de l’image de base
Évitez FROM golang:latest. En production, version fixe :
# Mauvais — comportement incertain
FROM golang:latest
# Bon — version explicite
FROM golang:1.21.5-alpine3.18
# Mieux — SHA256 fixe
FROM golang@sha256:abc123...
J’ai déjà eu un déploiement qui échouait soudainement : golang:latest mis à jour, incompatible avec nos dépendances. Depuis, versions fixées.
6. Ordre des COPY pour le cache
L’ordre compte. Fichiers stables d’abord, code métier ensuite :
# Bon ordre
FROM golang:1.21-alpine AS builder
WORKDIR /app
# 1. Fichiers de dépendances (rarement modifiés)
COPY go.mod go.sum ./
RUN go mod download
# 2. Code source (souvent modifié)
COPY . .
RUN go build -o myapp
# Mauvais ordre
COPY . . # Tout d'un coup
RUN go mod download && go build -o myapp
Première approche : modifier le code ne relance pas le téléchargement des dépendances. Deuxième : toute modification relance tout.
Principe central du cache — à retenir.
Guide de choix des images de base
Alpine, Slim, Distroless, Scratch — lequel choisir ? Tableau comparatif :
| Type d’image | Volume | Contenu | Avantages | Inconvénients | Cas d’usage |
|---|---|---|---|---|---|
| scratch | 0 Mo | Vide | Volume minimal, surface d’attaque minimale | Pas de shell, pas de debug, pas de CA | Go statique, Rust statique |
| distroless | 2-20 Mo | Libs runtime, CA | Pas de shell, sécurité, petit volume | Debug difficile | Go, Java, Rust, Node.js |
| alpine | 5-40 Mo | musl libc, gestionnaire de paquets | Petit, shell disponible | Problèmes compatibilité musl, DNS | Apps sans dépendance glibc |
| slim | 70-120 Mo | Debian/Ubuntu allégé | glibc complet, bonne compatibilité | Un peu plus gros | Apps avec dépendances C |
| image complète | 200 Mo+ | Système complet | Tous les outils | Gros volume, risque sécurité | Déconseillé en production |
Ma stratégie :
Go
- Préféré :
gcr.io/distroless/static-debian11(avec CA) - Alternative :
scratch(copier CA et timezone manuellement) - Avec CGO :
gcr.io/distroless/base-debian11oualpine
Java
- Préféré :
openjdk:17-jre-slim(JRE complet, bonne compatibilité) - Avancé :
gcr.io/distroless/java17-debian11(plus petit, debug difficile) - À éviter :
openjdk:17-alpine(bugs JVM sur Alpine)
Rust
- Préféré :
gcr.io/distroless/cc-debian11(runtime C) - Compilation statique pure :
scratch - À éviter : image
rustcomplète en production
Pièges Alpine
Alpine est souvent recommandé, mais attention :
- Compatibilité musl : musl au lieu de glibc — bugs étranges avec Java, Python
- DNS : résolution DNS parfois lente en Go, config spéciale nécessaire
- Fuseaux horaires : pas de données timezone par défaut, installer
tzdata
Mon conseil : si vous ne maîtrisez pas Alpine, -slim est plus sûr. Quelques dizaines de Mo de plus, moins de pièges.
Distroless
Images minimalistes Google : pas de shell, pas de gestionnaire de paquets, seulement le runtime.
Avantages :
- Surface d’attaque minimale (pas de shell = pas de commandes arbitraires)
- Petit volume
- Maintenance officielle, correctifs sécurité réguliers
Inconvénients :
- Debug difficile, pas de
docker exec -it - Préparer tous les fichiers en phase build
En production, j’utilise surtout Distroless. Debug via --target builder, production en Distroless.
Arbre de décision
Quel langage ?
├─ Go
│ ├─ Go pur → distroless/static ou scratch
│ └─ Avec CGO → distroless/base ou alpine
├─ Java
│ ├─ Stabilité → openjdk:jre-slim
│ └─ Volume → distroless/java
├─ Rust
│ ├─ Rust pur → distroless/cc ou scratch
│ └─ Avec libs C → distroless/cc
└─ Autre
└─ Commencer par slim, ajuster si problème
Couvre ~90 % des cas en pratique.
En résumé : trois points essentiels
L’essence du build multi-étapes :
- Phase build : image complète pour compiler
- Phase runtime : image allégée pour exécuter
- COPY —from : ne passer que les artefacts nécessaires
Simple — et les résultats sont spectaculaires : -70 à -90 % de volume, build 2 à 3 fois plus rapide avec cache optimisé, sécurité nettement améliorée.
Mon habitude : nouveau projet = Dockerfile multi-étapes dès le départ. Go, Java, Rust — réflexe automatique.
Quelques actions :
Aujourd’hui :
- Prendre un projet existant, tester le build multi-étapes
- Comparer avec
docker imagesavant/après - Si ça marche, généraliser aux autres projets
Pour approfondir :
- Documentation officielle Docker
- Outil
divepour analyser les couches (docker run --rm -it wagoodman/dive:latest your-image) - Fonctionnalités avancées BuildKit
Long terme :
- Cache d’images dans CI/CD
- Mises à jour régulières des images de base
- Scan sécurité (Trivy, etc.)
Pour finir : l’optimisation d’images Docker n’est pas difficile techniquement — le vrai défi, c’est la prise de conscience. Beaucoup d’équipes ont des images vieilles de plusieurs années, jamais revues, qui grossissent jusqu’à ce que le déploiement devienne insupportable.
N’attendez pas ce moment. Testez le build multi-étapes aujourd’hui — vous verrez à quel point les images peuvent être petites et les builds rapides.
Dites-moi en commentaire : quelle taille font vos images ? Quels gains après optimisation ? Quels pièges avez-vous rencontrés ?
Flux complet d'optimisation build multi-étapes Docker
Réduire les images Go/Java/Rust de GB à MB, avec Dockerfile complets et retours d'expérience
⏱️ Estimated time: 1 hr
- 1
Step 1: Comprendre le principe du build multi-étapes
Principe du build multi-étapes :
• Les langages compilés ont besoin de compilateurs et d'outils de build pour transformer le code source en exécutable
• Mais le runtime n'a pas besoin de tout ça
• Les Dockerfile traditionnels embarquent Maven, Gradle, le compilateur Go
• Comme emmener perceuse et ciment lors d'un déménagement — totalement inutile
Le cœur du problème :
• Les langages compilés ont besoin de compilateurs et d'outils de build
• Mais le runtime n'en a pas besoin
Étapes du build multi-étapes :
• Phase 1 (build) : image complète avec compilateur et outils, compilation du code source
• Phase 2 (runtime) : image minimale avec seulement le runtime, copie de l'exécutable depuis la phase 1
• L'image finale ne contient que le runtime et l'exécutable - 2
Step 2: Build multi-étapes Go en pratique
Build multi-étapes Go :
Phase 1 (build) :
• FROM golang:1.21 AS builder
• WORKDIR /app
• COPY go.mod go.sum ./
• RUN go mod download
• COPY . .
• RUN go build -o app
Phase 2 (runtime) :
• FROM alpine:latest
• RUN apk --no-cache add ca-certificates
• WORKDIR /root/
• COPY --from=builder /app/app .
• CMD ["./app"]
Résultats :
• Copie uniquement du binaire compilé
• Image de 295 Mo à 6,47 Mo (-98 %)
• Déploiement de 5 minutes à moins d'1 minute
• Volume d'image fortement réduit - 3
Step 3: Build multi-étapes Java et Rust en pratique
Build multi-étapes Java :
Phase 1 (build) :
• FROM maven:3.9 AS builder
• WORKDIR /app
• COPY pom.xml .
• RUN mvn dependency:go-offline
• COPY src ./src
• RUN mvn clean package -DskipTests
Phase 2 (runtime) :
• FROM eclipse-temurin:17-jre-alpine
• WORKDIR /app
• COPY --from=builder /app/target/app.jar app.jar
• CMD ["java", "-jar", "app.jar"]
Résultat : image de 650 Mo à 89 Mo (-86 %)
Build multi-étapes Rust :
Phase 1 (build) :
• FROM rust:1.75 AS builder
• WORKDIR /app
• COPY Cargo.toml Cargo.lock .
• RUN cargo fetch
• COPY src ./src
• RUN cargo build --release
Phase 2 (runtime) :
• FROM alpine:latest
• RUN apk --no-cache add ca-certificates
• WORKDIR /root/
• COPY --from=builder /app/target/release/app .
• CMD ["./app"]
Résultat : image de 2 Go à 11 Mo (-99,4 %) - 4
Step 4: Bonnes pratiques et optimisation long terme
Bonnes pratiques :
1. Choisir la bonne image runtime
• Go : distroless/static ou scratch
• Java : openjdk:jre-slim ou distroless/java
• Rust : distroless/cc ou scratch
2. Utiliser .dockerignore pour exclure les fichiers inutiles
3. Exploiter le cache de build (copier d'abord les fichiers de dépendances, puis le code source)
4. Mettre à jour régulièrement les images de base pour corriger les failles de sécurité
Optimisation long terme :
• Configurer le cache d'images dans le pipeline CI/CD
• Mettre à jour régulièrement les images de base
• Scanner les images avec un outil de sécurité (ex. Trivy)
L'optimisation d'images Docker n'est pas difficile techniquement — le vrai défi, c'est la prise de conscience. Beaucoup d'équipes ont des images créées il y a des années, jamais revues, qui grossissent jusqu'à ce que le déploiement devienne insupportable.
FAQ
Qu'est-ce que le build multi-étapes Docker ? Pourquoi en a-t-on besoin ?
Le cœur du problème : les langages compilés ont besoin de compilateurs, mais le runtime n'en a pas besoin.
Étapes :
• Phase 1 (build) : image complète avec compilateur et outils, compilation du code source
• Phase 2 (runtime) : image minimale avec seulement le runtime, copie de l'exécutable depuis la phase 1
• L'image finale ne contient que le runtime et l'exécutable
Quels sont les gains du build multi-étapes ?
• Go : de 295 Mo à 6,47 Mo (-98 %)
• Java Spring Boot : de 650 Mo à 89 Mo (-86 %)
• Rust : de 2 Go à 11 Mo (-99,4 %)
• Déploiement : de 5 minutes à moins d'1 minute
Cas concret : une application Spring Boot simple, image Docker de 650 Mo, après build multi-étapes seulement 89 Mo, déploiement de 5 minutes à moins d'1 minute.
Comment implémenter un build multi-étapes Go ?
Phase 1 avec golang:1.21 :
• FROM golang:1.21 AS builder
• WORKDIR /app
• COPY go.mod go.sum ./
• RUN go mod download
• COPY . .
• RUN go build -o app
Phase 2 avec alpine:latest :
• FROM alpine:latest
• RUN apk --no-cache add ca-certificates
• WORKDIR /root/
• COPY --from=builder /app/app .
• CMD ["./app"]
Copie uniquement du binaire compilé, image de 295 Mo à 6,47 Mo (-98 %). Déploiement de 5 minutes à moins d'1 minute.
Comment implémenter un build multi-étapes Java et Rust ?
Phase 1 avec maven:3.9 :
• FROM maven:3.9 AS builder
• WORKDIR /app
• COPY pom.xml .
• RUN mvn dependency:go-offline
• COPY src ./src
• RUN mvn clean package -DskipTests
Phase 2 avec eclipse-temurin:17-jre-alpine :
• FROM eclipse-temurin:17-jre-alpine
• WORKDIR /app
• COPY --from=builder /app/target/app.jar app.jar
• CMD ["java", "-jar", "app.jar"]
Image de 650 Mo à 89 Mo (-86 %)
Build multi-étapes Rust :
Phase 1 avec rust:1.75 :
• FROM rust:1.75 AS builder
• WORKDIR /app
• COPY Cargo.toml Cargo.lock .
• RUN cargo fetch
• COPY src ./src
• RUN cargo build --release
Phase 2 avec alpine:latest :
• FROM alpine:latest
• RUN apk --no-cache add ca-certificates
• WORKDIR /root/
• COPY --from=builder /app/target/release/app .
• CMD ["./app"]
Image de 2 Go à 11 Mo (-99,4 %)
Quelles sont les bonnes pratiques du build multi-étapes ?
1) Choisir la bonne image runtime :
• Go : distroless/static ou scratch
• Java : openjdk:jre-slim ou distroless/java
• Rust : distroless/cc ou scratch
2) Utiliser .dockerignore pour exclure les fichiers inutiles
3) Exploiter le cache de build (d'abord les dépendances, puis le code source)
4) Mettre à jour régulièrement les images de base
Optimisation long terme :
• Cache d'images dans le pipeline CI/CD
• Mises à jour régulières des images de base
• Scanner avec Trivy ou équivalent
L'optimisation n'est pas difficile techniquement — le vrai défi, c'est la prise de conscience.
14 min de lecture · Publié le: 17 déc. 2025 · Mis à jour le: 27 juil. 2026
Guide pratique Docker
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Optimisation Dockerfile : 5 astuces pour réduire la taille de l'image de 80 %
Vos images Docker pèsent plusieurs Go ? Maîtrisez Alpine, la fusion des RUN, le build multi-étapes, .dockerignore et le nettoyage du cache — passez de 1,2 Go à 180 Mo (-85 %). Cas Node.js complet et données mesurées.
Partie 4 sur 38
Suivant
Build multi-étapes Docker en pratique : passer d'une image de prod de 1 Go à 10 Mo
Maîtrisez le build multi-étapes Docker pour réduire une image de production de 1 Go à 10 Mo. Modèles Go, Node.js et Python, comparaison Alpine vs Distroless, et 5 pièges courants à éviter.
Partie 6 sur 38



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire