Multi-stage builds

Multi-stage builds

In the previous example, our final image contain:

  • our hello program

  • its source code

  • the compiler

Only the first one is strictly necessary.

We are going to see how to obtain an image without the superfluous components.

Multi-stage builds principles

  • At any point in our Dockerfile, we can add a new FROM line.

  • This line starts a new stage of our build.

  • Each stage can access the files of the previous stages with COPY –from=….

  • When a build is tagged (with docker build -t …), the last stage is tagged.

  • Previous stages are not discarded: they will be used for caching, and can be referenced.

Multi-stage builds in practice

Each stage is numbered, starting at 0

We can copy a file from a previous stage by indicating its number, e.g.

COPY --from=0 /file/from/first/stage /location/in/current/stage

We can also name stages, and reference these names

FROM golang AS builder
RUN ...
FROM alpine
COPY --from=builder /go/bin/mylittlebinary /usr/local/bin/

Multi-stage builds for our C program

We will change our Dockerfile to:

  • give a nickname to the first stage: compiler

  • add a second stage using the same ubuntu base image

  • add the hello binary to the second stage

  • make sure that CMD is in the second stage

The resulting Dockerfile is on the next slide.

Multi-stage build Dockerfile

Here is the final Dockerfile:

FROM ubuntu AS compiler
RUN apt-get update
RUN apt-get install -y build-essential
COPY hello.c /
RUN make hello
FROM ubuntu
COPY --from=compiler /hello /hello
CMD /hello

Let’s build it, and check that it works correctly:

Before the build

# docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
hello               latest              aae25a3dfa28        30 minutes ago      325MB
<none>              <none>              e43bb6363c1f        42 minutes ago      325MB
ubuntu              latest              452a96d81c30        4 weeks ago         79.6MB
# docker build -t hellomultistage .
Sending build context to Docker daemon  3.072kB
Step 1/8 : FROM ubuntu AS compiler
 ---> 452a96d81c30
Step 2/8 : RUN apt-get update
 ---> Using cache
 ---> 01e04143b340
Step 3/8 : RUN apt-get install -y build-essential
 ---> Using cache
 ---> 9139dae8927e
Step 4/8 : COPY hello.c /
 ---> Using cache
 ---> c803db9440ed
Step 5/8 : RUN make hello
 ---> Using cache
 ---> 2d25a58a49f0
Step 6/8 : FROM ubuntu
 ---> 452a96d81c30
Step 7/8 : COPY --from=compiler /hello /hello
 ---> d427a7aa53af
Step 8/8 : CMD /hello
 ---> Running in f338055a571e
Removing intermediate container f338055a571e
 ---> c8be88f00576
Successfully built c8be88f00576
Successfully tagged hellomultistage:latest
# docker run hellomultistage
Hello, big world!

Comparing single/multi-stage build image sizes

List our images with docker images, and check the size of:

  • the ubuntu base image (79.6MB)

  • the single-stage hello image (325MB)

  • the multi-stage hellomultistage image (79.6MB)

We can achieve even smaller images if we use smaller base images.

However, if we use common base images (e.g. if we standardize on ubuntu), these common images will be pulled only once per node, so they are virtually “free.”

docker images
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
hellomultistage     latest              c8be88f00576        About a minute ago   79.6MB
hello               latest              aae25a3dfa28        34 minutes ago       325MB
<none>              <none>              e43bb6363c1f        About an hour ago    325MB
ubuntu              latest              452a96d81c30        4 weeks ago          79.6MB