E-NO
DevOps 10 min de lecture

Builds multi-étapes Docker : concepts avancés et exemples pratiques pour la production

calendar_today Publié : 2026-09-10
update Dernière mise à jour : 2026-09-10
analytics Efficacité SEO : 100%
Illustration du guide technique pour « Builds multi-étapes Docker : concepts avancés et exemples pratiques pour la production ».

Introduction

Les builds multi-étapes Docker sont la méthode standard pour produire des images de petite taille, sécurisées et prêtes pour la production. L'idée de base est simple : utiliser une étape pour compiler ou préparer l'application, puis copier uniquement les artefacts nécessaires dans une image finale. Mais une utilisation avancée va bien au-delà d'un simple Dockerfile à deux étapes.

Cet article explore les techniques qui distinguent un pipeline de qualité production d'un projet amateur : contrôle précis du cache, builds conditionnels avec BuildKit, construction d'une étape cible spécifique, partage de données entre étapes avec --mount, et durcissement de l'image finale. Chaque concept est accompagné d'un exemple concret que vous pouvez exécuter dès aujourd'hui.

À la fin, vous saurez comment accélérer les builds, éviter les fuites de secrets et produire des images plus faciles à sécuriser et à maintenir.

Pourquoi les builds multi-étapes sont importants

Un Dockerfile à une seule étape pour une application Go pourrait ressembler à ceci :

FROM golang:1.22
WORKDIR /app
COPY . .
RUN go build -o myapp .
CMD ["./myapp"]

L'image résultante inclut toute la chaîne d'outils Go, le code source et le cache de build, dépassant souvent 800 Mo. En production, cela signifie des téléchargements plus lents, une surface d'attaque plus grande et un gaspillage d'espace disque.

Les builds multi-étapes résolvent ce problème en séparant l'environnement de build de l'environnement d'exécution. La même application peut être construite en moins de 20 Mo en utilisant une base scratch ou alpine :

# Étape 1 : build
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o myapp .

# Étape 2 : exécution
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/myapp .
CMD ["./myapp"]

Ce modèle est bien connu. Mais pour l'utiliser efficacement dans un vrai pipeline CI/CD, vous devez en comprendre les subtilités.

Stratégies de mise en cache avancées

Mise en cache des calques et ordre des instructions

Docker met en cache les calques en fonction de l'instruction et de ses entrées. Modifier une instruction invalide ce calque et tous les calques suivants. L'ordre est donc crucial.

Dans l'exemple Go ci-dessus, nous copions go.mod et go.sum avant de copier le reste du code source. Ainsi, le téléchargement des dépendances est mis en cache séparément des modifications du code de l'application. Si vous ne modifiez que main.go, le calque go mod download est réutilisé, économisant du temps réseau.

Une erreur courante est de tout copier en une seule fois :

COPY . .
RUN go mod download
RUN go build -o myapp .

Ici, toute modification d'un fichier invalide le calque de copie, ce qui entraîne la ré-exécution de go mod download. Séparez toujours l'installation des dépendances de la copie du code source.

Utilisation des montages de cache BuildKit

BuildKit (activé par défaut dans Docker 23.0+ avec le pilote Docker, ou via DOCKER_BUILDKIT=1) introduit des montages de cache qui persistent entre les builds sans stocker de données dans l'image finale. Ils sont parfaits pour les gestionnaires de paquets, les compilateurs et les artefacts de test.

Par exemple, pour mettre en cache les modules Go pendant un build :

# syntax=docker/dockerfile:1.7
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
    go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go build -o myapp .

La directive --mount=type=cache indique à BuildKit de monter un répertoire de cache persistant au chemin spécifié pendant cette instruction RUN. Le cache survit entre les builds (tant que l'ID du montage est stable) et n'est pas inclus dans l'image finale. Cela peut considérablement accélérer les builds.

Pour Node.js, vous pouvez mettre en cache node_modules ou le cache npm :

FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci
COPY . .
RUN npm run build

Ici, /root/.npm met en cache le cache des paquets npm, de sorte que la ré-exécution de npm ci ne re-télécharge pas les paquets à chaque fois.

Pour les images basées sur apt, vous pouvez mettre en cache /var/cache/apt et /var/lib/apt :

FROM debian:12 AS build
RUN --mount=type=cache,target=/var/cache/apt \
    --mount=type=cache,target=/var/lib/apt \
    apt-get update && apt-get install -y --no-install-recommends \
    build-essential

Cela évite de re-télécharger les listes de paquets à chaque build.

Gestion des secrets pendant le build

N'intégrez jamais de secrets dans une image. Même si vous les supprimez plus tard, ils restent dans les calques intermédiaires. BuildKit fournit des montages de secrets pour rendre les secrets disponibles uniquement pendant une instruction RUN spécifique.

Exemple : utiliser une clé SSH pour cloner un dépôt privé :

# syntax=docker/dockerfile:1.7
FROM alpine:3.19
RUN apk add --no-cache git openssh-client
RUN mkdir -p -m 0700 ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts
RUN --mount=type=ssh git clone [email protected]:myorg/private-repo.git /src

Construisez avec :

docker build --ssh default -t myapp .

Le socket de l'agent SSH est monté uniquement pour la commande git clone, et la clé privée ne se retrouve jamais dans l'image.

De même, vous pouvez monter un fichier secret :

RUN --mount=type=secret,id=mysecret cat /run/secrets/mysecret

Construisez avec :

docker build --secret id=mysecret,src=./secret.txt -t myapp .

C'est beaucoup plus sûr que d'utiliser ARG pour les secrets, car les valeurs ARG sont visibles dans l'historique de build.

Builds conditionnels avec BuildKit

Avec BuildKit, vous pouvez utiliser des montages RUN en ligne et des heredocs, et surtout, vous pouvez inclure conditionnellement des étapes en utilisant des arguments de build. Cela est utile pour créer des builds de débogage, sélectionner différentes architectures CPU ou activer/désactiver des fonctionnalités.

Considérons une image qui peut être construite soit comme image de développement (avec symboles de débogage et outils), soit comme image de production (minimale).

# syntax=docker/dockerfile:1.7
FROM alpine:3.19 AS base
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=builder /app/myapp .

FROM base AS production
CMD ["./myapp"]

FROM base AS development
RUN apk add --no-cache curl bash
CMD ["sh"]

Ensuite, construisez sélectivement :

docker build --target production -t myapp:prod .
docker build --target development -t myapp:dev .

Vous pouvez également utiliser des arguments de build pour choisir entre des alternatives dans une étape :

ARG VERSION=latest
FROM base AS builder
RUN if [ "$VERSION" = "debug" ]; then \
      go build -gcflags="all=-N -l" -o myapp .; \
    else \
      go build -o myapp .; \
    fi

Bien que les instructions if fonctionnent, elles peuvent devenir lourdes pour des conditions complexes. Une approche plus propre consiste à définir des étapes séparées et à sélectionner avec --target.

Question rapide 1 sur 2

Quel est un avantage principal de l'utilisation des builds multi-étapes dans Docker ?

La référence [1] indique que les builds multi-étapes vous permettent de réduire la taille de votre image finale en créant une séparation plus nette entre la construction de votre image et le résultat final.

Construire uniquement une étape spécifique

Dans un grand Dockerfile avec de nombreuses étapes, vous voulez souvent n'en construire qu'une seule. Par exemple, vous pourriez avoir une étape test qui exécute des tests unitaires, et vous voulez l'exécuter séparément de la construction de l'image finale.

# syntax=docker/dockerfile:1.7
FROM golang:1.22 AS base
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .

FROM base AS test
RUN go test ./...

FROM base AS build
RUN CGO_ENABLED=0 go build -o myapp .

FROM alpine:3.19 AS final
COPY --from=build /app/myapp /usr/local/bin/myapp
CMD ["myapp"]

Vous pouvez exécuter uniquement les tests avec :

docker build --target test .

Cela évite de construire l'image finale lorsque vous n'avez besoin que des résultats des tests. En CI, cela peut être utilisé dans un pipeline pour exécuter les tests avant de procéder à la construction.

Remarque : comme test dépend de base, Docker construira d'abord base, mais ne construira pas build ni final.

Partager des données entre étapes avec --mount

Parfois, vous devez transmettre plus que quelques fichiers entre les étapes. Les montages --mount=type=bind et --mount=type=cache de BuildKit offrent plus de flexibilité que COPY --from.

Par exemple, supposons que votre étape de build génère plusieurs artefacts (binaires, configurations, assets statiques) que vous voulez copier dans l'image finale, mais que vous voulez aussi exécuter une étape de post-traitement. Vous pourriez copier chaque artefact individuellement avec COPY --from, mais cela peut être verbeux.

Une alternative est d'utiliser un contexte nommé ou un montage bind depuis une étape précédente. Cependant, COPY --from reste la solution la plus simple dans la plupart des cas. La puissance de --mount réside dans le partage d'un répertoire pendant le build sans copier de fichiers dans un calque qui pourrait devenir obsolète.

Considérons un monorepo où l'étape de build compile un frontend et un backend, et qu'ils doivent partager du code commun. Vous pouvez monter en bind le répertoire source du contexte de build dans les deux étapes :

FROM node:20 AS frontend-build
WORKDIR /app
RUN --mount=type=bind,source=.,target=/app \
    npm install && npm run build:frontend

FROM golang:1.22 AS backend-build
WORKDIR /app
RUN --mount=type=bind,source=.,target=/app \
    go build -o server ./backend

Mais cette approche remonte tout le contexte pour chaque étape, ce qui peut ne pas être efficace. Il est généralement préférable d'utiliser COPY pour des fichiers spécifiques et de s'appuyer sur la mise en cache des calques.

Un cas d'utilisation spécial est --mount=type=cache partagé entre les étapes. Par exemple, vous pourriez vouloir réutiliser un cache de dépendances téléchargées entre plusieurs étapes de build :

# syntax=docker/dockerfile:1.7
FROM golang:1.22 AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go build -o myapp .

Le montage de cache est partagé entre les builds répétés, et même entre différentes étapes si elles utilisent le même chemin cible et le même ID. Mais notez que les montages de cache ne sont pas partagés entre les builds concurrents par défaut.

Aplatir les calques et réduire la taille de l'image

Bien que les builds multi-étapes produisent déjà des images plus petites, vous pouvez aller plus loin avec l'aplatissement des calques et COPY --link.

COPY --link (nécessite BuildKit) copie des fichiers d'une autre étape sans créer de dépendance sur les calques précédents. Cela peut permettre à la copie d'être effectuée en parallèle et réduit le nombre de calques dans certains cas.

# syntax=docker/dockerfile:1.7
FROM alpine:3.19 AS final
COPY --link --from=build /app/myapp /usr/local/bin/myapp

C'est particulièrement utile lorsque vous avez de nombreux petits fichiers à copier ; au lieu d'un calque par COPY, ils peuvent être combinés.

Pour réduire davantage la taille, envisagez d'utiliser scratch comme base finale si votre application est liée statiquement ou n'a pas besoin d'utilitaires système. Par exemple, un binaire Go construit avec CGO_ENABLED=0 peut s'exécuter sur scratch :

FROM scratch
COPY --from=build /app/myapp /myapp
ENTRYPOINT ["/myapp"]

Cela produit une image qui ne contient que le binaire, souvent moins de 10 Mo. Cependant, sachez que scratch n'a pas de shell, pas de certificats CA (sauf s'ils sont copiés) et pas de données de fuseau horaire. Vous devez inclure explicitement tout fichier nécessaire.

Si vous avez besoin de certificats CA, copiez-les depuis une étape de build :

FROM alpine:3.19 AS certs
RUN apk add --no-cache ca-certificates

FROM scratch
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /app/myapp /myapp
ENTRYPOINT ["/myapp"]

Cela garde l'image minuscule tout en permettant les appels HTTPS.

Durcissement de la sécurité

Les builds multi-étapes aident à la sécurité en séparant les outils de build de l'environnement d'exécution. Mais il y a d'autres mesures que vous pouvez prendre :

  1. Utilisez des versions spécifiques des images de base, pas latest. Épingler à alpine:3.19 ou debian:12-slim garantit la reproductibilité et réduit les surprises.
  2. Créez un utilisateur non root dans l'étape finale. Évitez d'exécuter les conteneurs en tant que root.
FROM alpine:3.19
RUN addgroup -S app && adduser -S app -G app
USER app
COPY --from=build --chown=app:app /app/myapp /usr/local/bin/myapp
CMD ["myapp"]
  1. Supprimez les permissions setuid/setgid inutiles et utilisez --no-install-recommends pour apt.
  2. Utilisez un scanner comme Trivy ou Clair pour identifier les vulnérabilités dans l'image finale.
  3. Définissez LABEL pour les métadonnées mais n'incluez pas d'informations sensibles.

Une autre technique puissante consiste à utiliser les drapeaux --sbom et --provenance de BuildKit pour générer des attestations, mais cela dépasse le cadre de cet article.

Question rapide 2 sur 2

Comment pouvez-vous copier des fichiers depuis une étape antérieure ou une image externe dans un build multi-étapes ?

La référence [2] mentionne que vous utilisez COPY --from pour copier des fichiers depuis des étapes antérieures ou des images externes.

Pièges courants et comment les éviter

  1. Ne pas ordonner les calques pour l'efficacité du cache. Erreur : copier tout le code source avant d'installer les dépendances. Impact : le cache est invalidé à chaque modification du code. Correction : copiez d'abord les fichiers de dépendances.
  1. Fuite de secrets via les arguments de build. Erreur : ARG mysecret puis l'utiliser dans un RUN. Correction : utilisez des montages de secrets ou des variables d'environnement au moment de l'exécution.
  1. Utiliser des images de base latest. Erreur : imprévisibilité entre les builds. Correction : épinglez à un digest ou une version spécifique.
  1. Construire des images en tant que root et les exécuter en tant que root. Erreur : la compromission du conteneur donne le root sur l'hôte. Correction : créez un utilisateur non root.
  1. Oublier de nettoyer dans l'étape finale. Erreur : copier des fichiers inutiles ou inclure des caches de gestionnaires de paquets. Correction : utilisez --no-cache et inspectez la taille de l'image.
  1. Ne pas utiliser BuildKit. Erreur : manquer les montages de cache, les montages de secrets et les builds parallèles. Correction : assurez-vous que DOCKER_BUILDKIT=1 ou Docker 23+ par défaut.
  1. Ignorer le nombre de calques. Erreur : trop de calques peuvent gonfler les métadonnées de l'image et ralentir les téléchargements. Correction : combinez les commandes RUN lorsque c'est judicieux et utilisez des copies multi-étapes.
  1. Utiliser COPY . . sans .dockerignore. Erreur : envoyer .git, des secrets locaux ou de gros fichiers au démon. Correction : créez un fichier .dockerignore.

Exemple de .dockerignore :

.git
.env
*.md
node_modules
  1. Ne pas tirer parti de --target pour les tests. Erreur : reconstruire toute l'image juste pour exécuter les tests. Correction : définissez une étape de test et utilisez docker build --target test .
  1. Supposer que le multi-étapes réduit automatiquement l'image. Erreur : si l'étape finale inclut des paquets inutiles, elle reste volumineuse. Correction : vérifiez régulièrement docker images et optimisez.

Exemple concret : frontend Node.js avec API backend

Parcourons un Dockerfile complet pour une application web typique : un frontend Node.js qui est construit en fichiers statiques, et un backend Go servant l'API. Nous voulons une image finale qui sert à la fois l'API et les fichiers statiques du frontend.

# syntax=docker/dockerfile:1.7

# ---- Build frontend ----
FROM node:20-alpine AS frontend-build
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build

# ---- Build backend ----
FROM golang:1.22-alpine AS backend-build
WORKDIR /app/backend
COPY backend/go.mod backend/go.sum ./
RUN go mod download
COPY backend/ .
RUN CGO_ENABLED=0 go build -o server .

# ---- Étape finale ----
FROM alpine:3.19
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=backend-build /app/backend/server ./server
COPY --from=frontend-build /app/frontend/dist ./static
EXPOSE 8080
USER nobody
CMD ["./server"]

Dans cet exemple, le frontend et le backend sont construits en parallèle (BuildKit peut exécuter des étapes indépendantes simultanément), et seuls le binaire compilé et les fichiers statiques sont copiés dans l'image finale. Le résultat est une petite image (probablement moins de 30 Mo) qui s'exécute en tant qu'utilisateur non root.

Pour construire et exécuter :

docker build -t myapp .
docker run -p 8080:8080 myapp

Le serveur backend doit être configuré pour servir les fichiers statiques depuis /app/static. Pour un serveur Go, cela peut signifier intégrer les fichiers statiques ou les servir via http.FileServer.

Liste de contrôle opérationnelle pour les builds multi-étapes

Avant de pousser une image en production, vérifiez les points suivants :

  • [ ] Les images de base sont épinglées par digest ou version spécifique (par exemple, alpine:3.19@sha256:...).
  • [ ] Le fichier .dockerignore exclut .git, les secrets, les journaux et les artefacts de build.
  • [ ] Aucun secret n'est transmis via --build-arg ; des montages de secrets ou des variables d'environnement d'exécution sont utilisés à la place.
  • [ ] L'image finale s'exécute en tant qu'utilisateur non root (instruction USER définie).
  • [ ] L'image ne contient que les fichiers nécessaires (vérifiez avec docker run --rm -it myimage sh et ls).
  • [ ] Les caches sont utilisés : les couches de dépendances sont séparées des modifications du code source.
  • [ ] BuildKit est activé et --mount=type=cache est utilisé lorsque c'est approprié.
  • [ ] Si vous utilisez scratch, les certificats CA et les données de fuseau horaire nécessaires sont inclus.
  • [ ] Le nombre de calques est raisonnable (combinez les commandes RUN liées).
  • [ ] La taille de l'image est mesurée et comparée aux builds précédents (docker images myapp).
  • [ ] Un scan de sécurité a été exécuté (par exemple, trivy image myapp).
  • [ ] Le build est reproductible : construire dans un environnement propre donne le même ID d'image (envisagez d'utiliser --provenance et --sbom).

Responsabilité : L'ingénieur DevOps (par exemple, Priya Shah, responsable de l'ingénierie) est le seul responsable de la définition du build multi-étapes et de sa revue. Le processus de build doit être revu au moins tous les trimestres ou chaque fois qu'une image de base change, qu'une nouvelle dépendance est ajoutée ou qu'un avis de sécurité affecte la pile.

Modes de défaillance et récupération

Même avec une planification minutieuse, des problèmes surviennent. Voici les défaillances courantes et comment les récupérer :

  1. Le build échoue en raison d'un secret ou d'un fichier manquant. Symptôme : ERROR: failed to solve: ... not found. Récupération : assurez-vous que le fichier secret existe et est transmis correctement (--secret ou --ssh). Vérifiez que le chemin à l'intérieur du conteneur est correct.
  1. Le cache n'est pas utilisé. Symptôme : le build re-télécharge toujours les dépendances. Récupération : vérifiez l'ordre des instructions et que les montages de cache sont correctement définis. Utilisez docker build --progress=plain pour voir les hits de cache. Assurez-vous que BuildKit est actif.
  1. Taille de l'image anormalement élevée. Symptôme : docker images montre une taille supérieure à celle attendue. Récupération : exécutez docker history <image> pour voir les tailles des calques. Identifiez les gros calques ; supprimez les fichiers inutiles ; envisagez une étape finale scratch ou alpine ; utilisez --no-install-recommends.
  1. Le conteneur plante avec not found pour les certificats CA ou le fuseau horaire. Récupération : copiez les certificats CA depuis une étape alpine comme montré précédemment ; définissez la variable d'environnement du fuseau horaire ou copiez tzdata.
  1. Permission refusée pour l'utilisateur non root. Symptôme : l'application ne peut pas écrire dans un répertoire. Récupération : assurez-vous que le répertoire appartient au même utilisateur/groupe en utilisant --chown dans COPY ou RUN chown.
  1. Secrets divulgués dans les calques de l'image. Récupération : faites immédiatement pivoter le secret. Reconstruisez l'image avec des montages de secrets. Envisagez d'utiliser le multi-étapes pour garantir que les secrets ne sont pas dans les calques finaux. Utilisez docker scan ou docker history pour détecter si nécessaire.
  1. La mise en cache des calques provoque des dépendances obsolètes. Symptôme : une ancienne version d'une dépendance est utilisée parce que le cache de calque est valide. Récupération : invalidez le cache en modifiant le fichier pertinent (par exemple, mettez à jour go.sum ou package-lock.json) ou utilisez --no-cache.
  1. Le montage de cache BuildKit remplit le disque. Symptôme : l'utilisation du disque de l'hôte de build augmente avec le temps. Récupération : purgez périodiquement le cache avec docker builder prune et définissez des limites de cache.

Conclusion

Les builds multi-étapes sont la pierre angulaire d'une utilisation efficace de Docker, mais leurs fonctionnalités avancées peuvent faire une différence significative en termes de vitesse de build, de sécurité et de maintenabilité. En appliquant les techniques de cet article — montages de cache, montages de secrets, ciblage sélectif d'étapes et gestion appropriée des utilisateurs — vous pouvez construire des images de production petites, rapides à construire et sécurisées.

La clé est de traiter le Dockerfile comme un élément d'infrastructure critique : versionnez-le, révisez-le et optimisez-le continuellement. Commencez par une amélioration : activez BuildKit, ajoutez un .dockerignore, utilisez des montages de cache pour votre gestionnaire de paquets et introduisez un utilisateur non root. Ensuite, mesurez l'impact et itérez.

Avec ces pratiques, vos conteneurs seront légers, vos builds rapides et votre surface d'attaque réduite.

Recherches connexes

Score de qualité de l’article

Utilité pour le lecteur 100%
  • check_circle Guide prêt à lire
  • check_circle Exemples pratiques inclus
  • check_circle URL d’article optimisée pour le SEO