Docker multi-stage builds tutorial

Last updated on  Dec 30, 2022  in  Docker  by  Amo Chen  ‐ 5 min read

The size of Docker images is also quite important in production environments.

If the Docker image is too large, not only will it occupy the transmission bandwidth, but it will also prolong the deployment time, so how to optimize the size of the Docker image is an important issue.

There are several ways to optimize the size of a Docker image, one of which is multi-stage build. However, the multi-stage builds example provided by the Docker official document does not work properly.

Requirements

Multi-stage builds

Before discussing multi-stage builds, let’s take a look at the following Dockerfile example:

FROM golang:1.16
WORKDIR /go/src/github.com/alexellis/href-counter/
COPY ./app.go ./
RUN GO111MODULE=off go get -d -v golang.org/x/net/html && \
    GO111MODULE=auto CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . && \
    apt-get install ca-certificates
CMD ["./app"]

The above Dockerfile attempts to compile alexellis/href-counter with a golang 1.16 Docker image, as seen in the example the RUN command downloads the go module and compiles the go executable. The ca-certificates package is also installed via apt-get, and ultimately, one executable file called ‘app’ is produced.

If you want to compile, please first download alexellis/href-counter, and modify the Dockerfile to the example above, the command is as follows:

$ git clone https://github.com/alexellis/href-counter
$ cd href-counter
$ vim Dockerfile

After compiling the example, it is approximately 989MB, which is quite large compared to the compiled executable app.

$ docker build -t alexellis2/href-counter:latest .
$ docker images
REPOSITORY                                                       TAG                                                     IMAGE ID       CREATED          SIZE
alexellis2/href-counter                                          latest                                                  2013ff215d52   29 seconds ago   989MB

The reason for such a large image is that the image of golang 1.16 itself occupies at least 900MB of space, and if a smaller image can be used, the image size can be effectively reduced.

If we further examine the compilation process, we can actually find that the whole process only requires the final executable file and the installation of the ca-certificates package. If we can put the finally compiled executable file and the installation of ca-certificates into a smallest Docker image, then the resulting image could be much smaller.

To achieve this goal, it can be broken down into the following two steps:

  1. Compile an executable file
  2. Move the executable produced in Step 1 into the appropriate Docker image file.

This is the concept of multi-stage builds.

Multi-stage builds

Multi-stage builds is a new feature supported by Docker Engine 17.05 and later versions. In addition to allowing developers to optimize Docker images, it also enables more structured and readable management of Dockerfiles. This is because multi-stage builds can break down Docker images into multiple steps.

multi-stage builds are useful to anyone who has struggled to optimize Dockerfiles while keeping them easy to read and maintain.

However, executing the example provided by the official Docker document will result in the following errors:

 => [internal] load build definition from Dockerfile                                                                            ...(skipped)...
 => CACHED [builder 4/5] COPY app.go    ./                                                                                      0.0s
 => ERROR [builder 5/5] RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .                                    0.5s
------
 > [builder 5/5] RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .:
#13 0.485 go: go.mod file not found in current directory or any parent directory; see 'go help modules'
------
executor failed running [/bin/sh -c CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .]: exit code: 1

The above errors are due to the fact that go modules are enabled by default after golang 1.16, so we need to make some slight modifications to turn off the go modules function before compilation can be successful.

The following Dockerfile example also requires you to download alexellis/href-counter first and modify its Dockerfile to the Docker multi-stage builds example below, with the following commands:

$ git clone https://github.com/alexellis/href-counter
$ cd href-counter

The following is an example of Docker multi-stage builds:

FROM golang:1.16 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN GO111MODULE=off go get -d -v golang.org/x/net/html
COPY app.go    ./
RUN GO111MODULE=auto CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app ./
CMD ["./app"]

The “AS builder” in the above Dockerfile example is the function supported by multi-stage builds, which allows developers to name the image compilation steps. “AS builder” names the image as builder, and the image is only responsible for compiling the executable app of go, which is

Next, COPY --from=builder /go/src/github.com/alexellis/href-counter/, a feature supported by multi-stage builds. It allows us to copy the executable file /go/src/github.com/alexellis/href-counter/app from the builder image (specified by --from=builder) to another Docker image located at /root/ path. The second image is based on Alpine, which is a very small image, and can significantly reduce the space occupied by the final image. This completes the multi-stage builds process.

The compilation instructions are as follows:

$ docker build -t alexellis2/href-counter:latest .

If the compilation is successful, you can enter the command docker images to check, and you can find that the final image only has a size of 12.7 MB.

$ docker images
REPOSITORY                                      TAG       IMAGE ID       CREATED         SIZE
alexellis2/href-counter                         latest    31063291ead7   2 days ago      12.7MB

Conclusion

Multi-stage builds is quite suitable for programming languages such as golang, C, C++ which require compilation. In addition to making the Dockerfile more structured, it can also effectively optimize the size of the Docker image when combined with the appropriate image file.

The official Docker documentation also mentions more about multi-stage builds, including how to do multi-stage builds with other methods before Docker Engine 17.05. If you have time, you can also read through the document.

That’s all, happy coding!

References

https://docs.docker.com/develop/develop-images/multistage-build/