SoFunction
Updated on 2025-03-03

Implementation of building a minimum Docker image for Golang applications

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!