⚠️ Traduction non officielle - Cette documentation est une traduction communautaire non officielle de Docker.

Constructions multi-étapes

Les constructions multi-étapes sont utiles à quiconque a eu du mal à optimiser les Dockerfiles tout en les gardant faciles à lire et à maintenir.

Utiliser les constructions multi-étapes

Avec les constructions multi-étapes, vous utilisez plusieurs instructions FROM dans votre Dockerfile. Chaque instruction FROM peut utiliser une base différente, et chacune d'elles commence une nouvelle étape de la construction. Vous pouvez copier sélectivement des artefacts d'une étape à une autre, en laissant derrière tout ce que vous ne voulez pas dans l'image finale.

Le Dockerfile suivant a deux étapes distinctes : une pour construire un binaire, et une autre où le binaire est copié de la première étape à l'étape suivante.

# syntax=docker/dockerfile:1
FROM golang:1.24
WORKDIR /src
COPY <<EOF ./main.go
package main

import "fmt"

func main() {
  fmt.Println("hello, world")
}
EOF
RUN go build -o /bin/hello ./main.go

FROM scratch
COPY --from=0 /bin/hello /bin/hello
CMD ["/bin/hello"]

Vous n'avez besoin que du seul Dockerfile. Pas besoin d'un script de construction séparé. Exécutez simplement docker build.

$ docker build -t hello .

Le résultat final est une petite image de production ne contenant que le binaire. Aucun des outils de construction requis pour construire l'application n'est inclus dans l'image résultante.

Comment ça marche ? La deuxième instruction FROM commence une nouvelle étape de construction avec l'image scratch comme base. La ligne COPY --from=0 copie juste l'artefact construit de l'étape précédente dans cette nouvelle étape. Le SDK Go et tous les artefacts intermédiaires sont laissés pour compte et ne sont pas enregistrés dans l'image finale.

Nommez vos étapes de construction

Par défaut, les étapes ne sont pas nommées, et vous vous y référez par leur numéro entier, en commençant par 0 pour la première instruction FROM. Cependant, vous pouvez nommer vos étapes, en ajoutant un AS <NAME> à l'instruction FROM. Cet exemple améliore le précédent en nommant les étapes et en utilisant le nom dans l'instruction COPY. Cela signifie que même si les instructions de votre Dockerfile sont réorganisées plus tard, la COPY ne se casse pas.

# syntax=docker/dockerfile:1
FROM golang:1.24 AS build
WORKDIR /src
COPY <<EOF /src/main.go
package main

import "fmt"

func main() {
  fmt.Println("hello, world")
}
EOF
RUN go build -o /bin/hello ./main.go

FROM scratch
COPY --from=build /bin/hello /bin/hello
CMD ["/bin/hello"]

S'arrêter à une étape de construction spécifique

Lorsque vous construisez votre image, vous n'avez pas nécessairement besoin de construire l'intégralité du Dockerfile, y compris chaque étape. Vous pouvez spécifier une étape de construction cible. La commande suivante suppose que vous utilisez le Dockerfile précédent mais s'arrête à l'étape nommée build :

$ docker build --target build -t hello .

Quelques scénarios où cela pourrait être utile sont :

  • Débogage d'une étape de construction spécifique
  • Utilisation d'une étape debug avec tous les symboles de débogage ou outils activés, et une étape production allégée
  • Utilisation d'une étape testing dans laquelle votre application est peuplée de données de test, mais construire pour la production en utilisant une étape différente qui utilise des données réelles

Utiliser une image externe comme étape

Lorsque vous utilisez des constructions multi-étapes, vous n'êtes pas limité à copier depuis les étapes que vous avez créées plus tôt dans votre Dockerfile. Vous pouvez utiliser l'instruction COPY --from pour copier depuis une image distincte, soit en utilisant le nom de l'image locale, une balise disponible localement ou sur un registre Docker, ou un ID de balise. Le client Docker tire l'image si nécessaire et copie l'artefact à partir de là. La syntaxe est :

COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

Utiliser une étape précédente comme nouvelle étape

Vous pouvez reprendre là où une étape précédente s'est arrêtée en vous y référant lorsque vous utilisez la directive FROM. Par exemple :

# syntax=docker/dockerfile:1

FROM alpine:latest AS builder
RUN apk --no-cache add build-base

FROM builder AS build1
COPY source1.cpp source.cpp
RUN g++ -o /binary source.cpp

FROM builder AS build2
COPY source2.cpp source.cpp
RUN g++ -o /binary source.cpp

Différences entre l'ancien constructeur et BuildKit

L'ancien constructeur Docker Engine traite toutes les étapes d'un Dockerfile menant à la --target sélectionnée. Il construira une étape même si la cible sélectionnée ne dépend pas de cette étape.

BuildKit ne construit que les étapes dont dépend l'étape cible .

Par exemple, étant donné le Dockerfile suivant :

# syntax=docker/dockerfile:1
FROM ubuntu AS base
RUN echo "base"

FROM base AS stage1
RUN echo "stage1"

FROM base AS stage2
RUN echo "stage2"

Avec BuildKit activé, construire la cible stage2 dans ce Dockerfile signifie que seules base et stage2 sont traitées. Il n'y a pas de dépendance sur stage1, donc elle est ignorée.

$ DOCKER_BUILDKIT=1 docker build --no-cache -f Dockerfile --target stage2 .
[+] Building 0.4s (7/7) FINISHED                                                                    
 => [internal] load build definition from Dockerfile                                            0.0s
 => => transferring dockerfile: 36B                                                             0.0s
 => [internal] load .dockerignore                                                               0.0s
 => => transferring context: 2B                                                                 0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest                                0.0s
 => CACHED [base 1/2] FROM docker.io/library/ubuntu                                             0.0s
 => [base 2/2] RUN echo "base"                                                                  0.1s
 => [stage2 1/1] RUN echo "stage2"                                                              0.2s
 => exporting to image                                                                          0.0s
 => => exporting layers                                                                         0.0s
 => => writing image sha256:f55003b607cef37614f607f0728e6fd4d113a4bf7ef12210da338c716f2cfd15    0.0s

D'un autre côté, construire la même cible sans BuildKit entraîne le traitement de toutes les étapes :

$ DOCKER_BUILDKIT=0 docker build --no-cache -f Dockerfile --target stage2 .
Sending build context to Docker daemon  219.1kB
Step 1/6 : FROM ubuntu AS base
 ---> a7870fd478f4
Step 2/6 : RUN echo "base"
 ---> Running in e850d0e42eca
base
Removing intermediate container e850d0e42eca
 ---> d9f69f23cac8
Step 3/6 : FROM base AS stage1
 ---> d9f69f23cac8
Step 4/6 : RUN echo "stage1"
 ---> Running in 758ba6c1a9a3
stage1
Removing intermediate container 758ba6c1a9a3
 ---> 396baa55b8c3
Step 5/6 : FROM base AS stage2
 ---> d9f69f23cac8
Step 6/6 : RUN echo "stage2"
 ---> Running in bbc025b93175
stage2
Removing intermediate container bbc025b93175
 ---> 09fc3770a9c4
Successfully built 09fc3770a9c4

L'ancien constructeur traite stage1, même si stage2 n'en dépend pas.