Eighteen postures for constructing Go application docker image

Cultivation background

I worked overtime day and night to develop the simplest Go Hello world application. Although I just quit after printing, my boss also asked me to go online, the only application I can write.

The project structure is as follows:

.
├── go.mod
└── hello.go

hello.go code is as follows:

package main

func main() {
    println("hello world!")
}

Moreover, the boss asked us to deploy with docker, which makes us follow the trend and be taller...

First attempt

After visiting some Wulin friends, I found that it would be better to put the whole process into docker and compile it. After thinking about it, I got the following Dockerfile:

FROM golang:alpine

WORKDIR /build

COPY hello.go .

RUN go build -o hello hello.go

CMD ["./hello"]

Build image:

$ docker build -t hello:v1 .

Done. Let's get closer.

$ docker run -it --rm hello:v1 ls -l /build
total 1260
-rwxr-xr-x    1 root     root       1281547 Mar  6 15:54 hello
-rw-r--r--    1 root     root            55 Mar  6 14:59 hello.go

Good guy, the code I finally wrote is also in it. It seems that the code can't be written badly, otherwise the operation and maintenance sister will peek and laugh at me...

Let's see how big the image is. It's said that pulling the image will be slower when it's big

$ docker images | grep hello
hello         v1    2783ee221014   44 minutes ago   314MB

Wow, there's 314MB. Has docker build changed into Java? Not everything is as big as possible...

Let's see why it's so big!

Look, we had 300+MB before the first instruction (WORKDIR). It's a little fierce!

Anyway, let's run first

$ docker run -it --rm hello:v1
hello world!

No problem, at least you can work ~

Second attempt

After some smoking and drinking, plus the advice of friends, we found that the basic image we used was too big.

$ docker images | grep golang
golang    alpine     d026981a7165   2 days ago          313MB

And my friend told me that I can compile the code first and then copy it in, so I don't need the huge basic image. However, it's easy to say, I still spent some time. Finally, the Dockerfile looks like this:

FROM alpine

WORKDIR /build

COPY hello .

CMD ["./hello"]

Run and try

$ docker build -t hello:v2 .
...
=> ERROR [3/3] COPY hello .                         0.0s
------
 > [3/3] COPY hello .:
------
failed to compute cache key: "/hello" not found: not found

No, hello can't be found. Forget to compile hello first Go, come again~

$ go build -o hello hello.go

Then run docker build -t hello:v2, Try... Two steps, no problem...

$ docker run -it --rm hello:v2
standard_init_linux.go:228: exec user process caused: exec format error

Failed! Well, the format is wrong. Originally, our development machine is not linux. Come again~

$ GOOS=linux go build -o hello hello.go

The docker build is finally finished. Run down quickly

$ docker run -it --rm hello:v2
hello world!

No problem. Let's see the content and size.

$ docker run -it --rm hello:v2 ls -l /build
total 1252
-rwxr-xr-x    1 root     root       1281587 Mar  6 16:18 hello

There is only the executable file hello, so I don't have to worry about being despised by others~

$ docker images | grep hello
hello    v2   0dd53f016c93   53 seconds ago      6.61MB
hello    v1   ac0e37173b85   25 minutes ago      314MB

Wow, 6.61MB, absolutely!

Look, we only have 5.3MB in front of the first instruction (WORKDIR). Happy!

Third attempt

After a show off, someone despised me and said that multi-stage construction is popular now. What's the problem with the second method? After careful consideration, we found that we should be able to build a docker image from the Go code, which is divided into three steps:

  1. If cgo cross platform compilation is involved in the native compilation of Go code, it will be more troublesome
  2. Build docker image with compiled executable file
  3. Write a shell script or makefile so that these steps can be obtained through a command

Multistage construction is to put all this into a Dockerfile. There is no source code leakage, and there is no need to use scripts to compile across platforms. It also obtains the smallest image.

Loving learning and pursuing perfection, I finally wrote the following Dockerfile, one more line is fat and one less line is thin:

FROM golang:alpine AS builder

WORKDIR /build

ADD go.mod .
COPY . .
RUN go build -o hello hello.go


FROM alpine

WORKDIR /build
COPY --from=builder /build/hello /build/hello

CMD ["./hello"]

The first from part is to build a builder image, in which the purpose is to compile the executable file hello. The second from part is to copy the executable file Hello from the first image, and use the smallest possible basic image Alpine to ensure that the final image is as small as possible. As for why not use a smaller scratch, because scratch really has nothing, There is no chance to take a look at the problem, and alpine is only 5MB, which will not have much impact on our service.

We ran first to verify:

$ docker run -it --rm hello:v3
hello world!

No problem, as expected! Look at the size:

$ docker images | grep hello
hello    v3     f51e1116be11   8 hours ago    6.61MB
hello    v2     0dd53f016c93   8 hours ago    6.61MB
hello    v1     ac0e37173b85   8 hours ago    314MB

It is exactly the same size as the image built by the second method. Look at the content in the image:

$ docker run -it --rm hello:v3 ls -l /build
total 1252
-rwxr-xr-x    1 root     root       1281547 Mar  6 16:32 hello

There is only one executable hello file, perfect!

It is basically the same as the second final image, but we have simplified the process. We only need a Dockerfile and run a command. I don't need to fix those obscure shell s and makefile s.

Practice divine skill

So far, the team partners feel perfect and praise me one after another! However, I think I'm not only pursuing perfection but also lazy (fishing). Every time I'm asked to write such a Dockerfile that one line is fat and one line is thin, I still feel very annoyed, so I wrote a tool without telling my boss, and I'll show it~~

# Install it first
$ GOPROXY=https://goproxy.cn/,direct go install github.com/zeromicro/go-zero/tools/goctl@latest
# Write Dockerfile with one click
$ goctl docker -go hello.go

Done! Look at the generated Dockerfile

FROM golang:alpine AS builder

LABEL stage=gobuilder

ENV CGO_ENABLED 0
ENV GOPROXY https://goproxy.cn,direct

RUN apk update --no-cache && apk add --no-cache tzdata

WORKDIR /build

ADD go.mod .
ADD go.sum .
RUN go mod download
COPY . .
RUN go build -ldflags="-s -w" -o /app/hello ./hello.go


FROM alpine

RUN apk update --no-cache && apk add --no-cache ca-certificates
COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo/Asia/Shanghai
ENV TZ Asia/Shanghai

WORKDIR /app
COPY --from=builder /app/hello /app/hello

CMD ["./hello"]

Here are some points:

  • cgo is disabled by default
  • GOPROXY acceleration go mod download enabled
  • Debugging information - ldflags="-s -w" is removed to reduce the image size
  • CA certificates is installed so that you can use TLS certificates
  • tzdata is installed in the builder image, and only the required time zone is copied in the final image
  • The local time zone is automatically set, so what we see in the log is Beijing time

Let's take a look at the image size built with this automatically generated Dockerfile:

$ docker images | grep hello
hello     v4    94ba3ece3071   4 hours ago     6.66MB
hello     v3    f51e1116be11   8 hours ago     6.61MB
hello     v2    0dd53f016c93   8 hours ago     6.61MB
hello     v1    ac0e37173b85   9 hours ago     314MB

Slightly larger because we copied CA certificates and tzdata. Verify:

Let's see what's in the mirror:

$ docker run -it --rm hello:v4 ls -l /app
total 832
-rwxr-xr-x    1 root     root        851968 Mar  7 08:36 hello

There are only hello executable files, and the file size has been reduced from 1281KB to 851KB. Run and see:

$ docker run -it --rm hello:v4
hello world!

And you can specify the basic image as scratch when generating Dockerfile, so the image is smaller, but you can't log in directly through sh.

$ goctl docker -base scratch -go hello.go

The size is really small:

$ docker images | grep hello
hello   v5   d084eed88d88   4 seconds ago   1.07MB
hello   v4   94ba3ece3071   15 hours ago    6.66MB
hello   v3   f51e1116be11   4 days ago      6.61MB
hello   v2   0dd53f016c93   4 days ago      6.61MB
hello   v1   ac0e37173b85   4 days ago      314MB

Look at what's in the mirror

I compiled the image of linux/arm64 on Macbook M1. I guess you usually need to use the image of linux/amd64. Just use the following command:

$ docker build --rm --platform linux/amd64 -t hello:v6 .

All right, all right, stop pestering Dockerfile. I'm going to learn new skills ~

Project address

https://github.com/zeromicro/go-zero

Do you think it's good? Welcome to call for rewards. Just light up the little star of GitHub ⭐ ️

Wechat communication group

Follow the "microservice practice" official account and click the exchange group to obtain the community group QR code.

Tags: Go Docker Microservices dockerfile go-zero

Posted by Jeller on Fri, 20 May 2022 05:47:20 +0300