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.
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:
- Compile an executable file
- Move the executable produced in Step 1 into the appropriate Docker image file.
This is the concept of 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
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
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!