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 étapeproduction
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.