I usually use docker to run my golang program, and here I share my experience building docker images. I build a docker image not only optimizes the built volume, but also optimizes the build speed.
Sample Application
First post a code example, let's assume that you want to build an http service
package main import ( "fmt" "net/http" "time" "/gin-gonic/gin" ) func main() { ("Server Ready") router := () ("/", func(c *) { (200, "hello world, this time is: "+().Format(time.RFC1123Z)) }) ("/github", func(c *) { _, err := ("/") if err != nil { (500, ()) return } (200, "access github api ok") }) if err := (":9900"); err != nil { panic(err) } }
illustrate:
- Gin is selected as an example here to demonstrate that we need to optimize the build speed under the conditions of having third-party packages.
- The first line of main function prints a line of words to demonstrate a pitfall encountered during startup later
- Print time with the route to demonstrate the pitfalls about time zones encountered later
- Routing github attempts to access, to demonstrate the certificate pitfall encountered later
Here we can try the volume of the package after building
$ go build -o server $ ls -alh | grep server -rwxrwxrwx 1 eyas eyas 14.6M May 29 10:26 server
14.6MB, this is a hello world for http service. Of course, this is because gin is used, so it is a bit large. If hello world written in the standard package net/http, the volume is about 7 MB
The evolution of Dockerfile
Version 1, preliminary optimization
Let's take a look at the first version
FROM golang:1.14-alpine as builder WORKDIR /usr/src/app ENV GOPROXY= COPY ./ ./ COPY ./ ./ RUN go mod download COPY . . RUN go build -ldflags "-s -w" -o server FROM scratch as runner COPY --from=builder /usr/src/app/server /opt/app/ CMD ["/opt/app/server"]
illustrate:
- Golang:1.14-alpine is selected as the compilation environment because this is the smallest golang compilation environment
- GOPROXY is set to improve build speed
- First copy and then go mod download to prevent the dependency package from being re-downloaded every time the build is used to improve the build speed using the docker build cache.
- When go build, add -ldflags "-s -w" to remove the debugging information of the build package, and reduce the volume of the program after go build, which can be reduced by about 1/4.
- Multi-stage construction is used, that is, FROM XXX as xxx. When building the package, it uses a mirror with a compilation environment to build it. When running, it does not require a compilation environment of go at all, so it is used to run it with an empty mirror scratch of docker in the run phase. This is the most effective way to reduce the image volume.
OK, let's start building the image
$ docker build -t server . ... Successfully built 8d3b91210721 Successfully tagged server:latest
At this point, the construction is successful and check the image size
$ docker images server latest 8d3b91210721 1 minutes ago 11MB
11MB, OK, run it now
$ docker run -p 9900:9900 server standard_init_linux.go:211: exec user process caused "no such file or directory"
I found that an error was reported in the startup, and the first line of the main function print statement did not appear, so the entire program was not running at all. The reason for the error is the lack of library dependency files. This is actually a built go program that also relies on the underlying so library file. If you don't believe it, you can check its dependencies after the physical machine is compiled.
$ go build -o server $ ldd server .1 (0x00007ffcfb775000) .0 => /lib/x86_64-linux-gnu/.0 (0x00007f9a8dc47000) .6 => /lib/x86_64-linux-gnu/.6 (0x00007f9a8d856000) /lib64/.2 (0x00007f9a8de66000)
Is this a bit different from our perception? It is said that there are no dependencies, but there are still several dependency library files. Although these dependencies are at the lowest level and generally have them in the operating system, who asked us to choose scratch? There is really nothing in this image except the Linux kernel.
This is because go build enables CGO by default. If you don’t believe it, you can try this command go env CGO_ENABLED. When CGO is enabled, no matter whether the code uses CGO or not, there will be a library dependency file. The solution is also very simple. Just manually specify to close CGO, and the package size will not increase, and it will also decrease.
$ CGO_ENABLED=0 go build -o server $ ldd server not a dynamic executable
Version 2, solve the error reported during runtime
FROM golang:1.14-alpine as builder WORKDIR /usr/src/app ENV GOPROXY= COPY ./ ./ COPY ./ ./ RUN go mod download COPY . . -RUN go build -ldflags "-s -w" -o server +RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server FROM scratch as runner COPY --from=builder /usr/src/app/server /opt/app/ CMD ["/opt/app/server"]
Change point: CGO_ENABLED=0 was added before go build
$ docker build -t server . ... Successfully built a81385160e25 Successfully tagged server:latest $ docker run -p 9900:9900 server [GIN-debug] GET / --> .func1 (3 handlers) [GIN-debug] GET /github --> .func2 (3 handlers) [GIN-debug] Listening and serving HTTP on :9900
It starts normally, let's try to visit it, check the current time before accessing
$ date Fri May 29 13:11:28 CST 2020 $ curl http://localhost:9900 hello world, this time is: Fri, 29 May 2020 05:18:28 +0000 $ curl http://localhost:9900/github Get "/": x509: certificate signed by unknown authority
I found something wrong
- The current system time is 13:11:28, but according to the time displayed by it is 05:11:53, it is actually the time zone in the docker container is wrong. The default is 0 time zone, but our country is East 8 District
- Try to access/This is the https site, and the certificate error is reported
Solve the problem
- Place root certificate in container
- Set container time zone
Version 3, solve the problem of running environment time zone and certificate
FROM golang:1.14-alpine as builder WORKDIR /usr/src/app ENV GOPROXY= +RUN sed -i 's///g' /etc/apk/repositories && \ + apk add --no-cache ca-certificates tzdata COPY ./ ./ COPY ./ ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server FROM scratch as runner +COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime +COPY --from=builder /etc/ssl/certs/ /etc/ssl/certs/ COPY --from=builder /usr/src/app/server /opt/app/ CMD ["/opt/app/server"]
During the builder stage, two libraries of ca-certificates tzdata were installed. In the runner stage, a copy of the time zone configuration and root certificate were copied.
$ docker build -t server . ... Successfully built e0825838043d Successfully tagged server:latest $ docker run -p 9900:9900 server [GIN-debug] GET / --> .func1 (3 handlers) [GIN-debug] GET /github --> .func2 (3 handlers) [GIN-debug] Listening and serving HTTP on :9900
Try visiting
$ date Fri May 29 13:27:16 CST 2020 $ curl http://localhost:9900 hello world, this time is: Fri, 29 May 2020 13:27:16 +0800 $ curl http://localhost:9900/github access github api ok
Everything is working fine, check the current image size
$ docker images server latest e0825838043d 9 minutes ago 11.3MB
It's only 11.3MB, which is already very small, but it can be even smaller, which means compressing the built package again
Version 4, further reducing volume
FROM golang:1.14-alpine as builder WORKDIR /usr/src/app ENV GOPROXY= RUN sed -i 's///g' /etc/apk/repositories && \ - apk add --no-cache ca-certificates tzdata + apk add --no-cache upx ca-certificates tzdata COPY ./ ./ COPY ./ ./ RUN go mod download COPY . . -RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server +RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server &&\ + upx --best server -o _upx_server && \ + mv -f _upx_server server FROM scratch as runner COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime COPY --from=builder /etc/ssl/certs/ /etc/ssl/certs/ COPY --from=builder /usr/src/app/server /opt/app/ CMD ["/opt/app/server"]
In the builder stage, upx is installed and go build is completed, and you use upx to compress it and execute the build. You will find that the build time has become longer. This is because the parameter I set for upx is --best, that is, the maximum compression level, so that the compression level will be as small as possible after compression. If it is slow, you can reduce the compression level from -1 to -9. The larger the number, the higher the compression level, and the slower it is. I use --best to see the mirror volume after the build is finished.
$ docker build -t server . ... Successfully built 80c3f3cde1f7 Successfully tagged server:latest $ docker images server latest 80c3f3cde1f7 1 minutes ago 4.26MB
Now it's so small, it's only 4.26MB. Let's try those two interfaces again, everything is normal. The optimization ends here.
The final Dockerfile
FROM golang:1.14-alpine as builder WORKDIR /usr/src/app ENV GOPROXY= RUN sed -i 's///g' /etc/apk/repositories && \ apk add --no-cache upx ca-certificates tzdata COPY ./ ./ COPY ./ ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o server &&\ upx --best server -o _upx_server && \ mv -f _upx_server server FROM scratch as runner COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /etc/localtime COPY --from=builder /etc/ssl/certs/ /etc/ssl/certs/ COPY --from=builder /usr/src/app/server /opt/app/ CMD ["/opt/app/server"]
Summarize
To reduce the image volume, it is important to build multiple stages first, so that the compilation environment and the running environment can be separated.
In addition, it is actually unwise to choose the scratch image. Although it is very small, it is too primitive and has no tools inside. After the program is started, it cannot even enter the container, and even if it is entered, it cannot do anything. So even if I just pursue the smallest mirror size possible, it is not recommended to choose scratch as the operating environment. I have only stepped on a small part of the pit, and there are more pits behind it. I have no interest in continuing to step on scratch.
It is recommended to choose alpine. The mirror size of alpine is 5.61MB. This size is actually the size after the image is decompressed. In fact, when downloading the image, you only need to download 2.68MB. Also, all the mirror volumes I mentioned above refer to the decompressed mirror volume, which is different from the actual volume when uploading. Docker will compress it once and then transmit the mirror.
There is also a very small image that is busybox. Its size is 1.22MB and it is downloaded 705.6 KB. Most of the Linux commands are available, but the running environment is still very primitive. If you are interested, you can try it.
Whether it is alpine or busybox, they will have the above time zone and certificate problems. They can also solve them by following the above method. Switching to alpine or busybox is also very simple. You only need to modify the runner basic image.
-FROM scratch as runner +FROM alpine as runner
or
-FROM scratch as runner +FROM busybox as runne
This is the end of this article about building the implementation of Golang application's minimum Docker image. For more related content on Golang's minimum Docker image, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!