The martial arts of writing Go Dockerfile
Background
I worked around the clock and overtime to develop the simplest Go
Hello world
application, and although I just ran a print and quit, my boss also asked me to go live with the only application I could write.The project layout is as follows.
. ├── go.mod └── hello.go
hello.go
code as follows:package mainfuncmain() { println("hello world!") }
And, the boss asked for
docker
deployment to make us look up-to-date.First try
After visiting some friends for the martial arts, I found that it would be good to compile the whole process into
docker
, and after a bit of work, I got the following Dockerfile
:FROM golang:alpineWORKDIR /buildCOPY hello.go .RUN go build -o hello hello.goCMD [". /hello"]
To build the image.
$ docker build -t hello:v1 .
Done, let’s get a closer look.
$ 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
All my code is packed into the image, it looks like I can’t write bad code, otherwise the ops girl will laugh at me if she read my code.
We look at what’s the size of the mirror, it is said that the larger image, the slower to pull.
$ docker docker images | grep hello hello v1 2783ee221014 44 minutes ago 314MB
Wow, 314MB, did
docker build
make the Go
program become Java
?Let’s see why it’s so large!
Look, we’ve got 300+MB before we run the first instruction (
WORKDIR
), that's a bit fierce!Anyway, let’s run it and see
$ docker run --it --rm hello:v1 hello world!
No problem, at least it works!
Second try
After a lot of research, and some advices from friends, we found that the base image we were using was too big.
$ docker images | grep golang golang alpine d026981a7165 2 days ago 313MB
And my friend told me that I could compile the code first and then copy it in without the huge base image, but that’s easier said than done, so I put some effort into it and the final
Dockerfile
looks like this.FROM alpineWORKDIR /buildCOPY hello .CMD [". /hello"]
Try running
$ 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
was not found, I forgot to compile hello.go
first, come again~$ go build -o hello hello.go
Then run
docker build -t hello:v2 .
, no problem, take two steps and try.$ docker run -it --rm hello:v2 standard_init_linux.go:228: exec user process caused: exec format error
Failed! Well, the format is not right, so our development machine is not
linux
, ah, again~$ GOOS=linux go build -o hello hello.go
Rerun
docker build
, finally done, hurry up and run$ 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
hello
executable inside, no more worrying about people despising my code~$ 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 got 5.3MB in front of the first command (
WORKDIR
), happy!Third try
After a bit of showing off, someone actually despised me and said that it is now popular to build in multiple stages, so what is wrong with the second way? After thinking about it, I found that we need to be able to build a
docker
image from Go
code, which is divided into three steps.- compile the
Go
code natively, if it involvescgo
cross-platform compilation will be more troublesome
- build the
docker
image with the compiled executable
- write a
shell
script ormakefile
to make these steps available via a single command
A multi-stage build puts it all into a
Dockerfile
, no source code leaks, no scripting to compile across platforms, and a minimal image.I finally wrote the following
Dockerfile
, no one line more, no one line less.FROM golang:alpine AS builderWORKDIR /buildADD go.mod . COPY . . RUN go build -o hello hello.go FROM alpineWORKDIR /build COPY --from=builder /build/hello /build/helloCMD [". /hello"]
The first
FROM
starts by building a builder
image to compile the executable hello
in, and the second From
starts by copying the executable hello
from the first image and using the smallest possible base image alpine
to keep the final image as small as possible, as to the reason why we don't use the smaller scratch
is that scratch
really has nothing, so we don't even have a chance to look at it if there's a problem, and alpine
is only 5MB, so it won't have much impact on our service. And I’ll talk about generating the images from scratch
later in this article.Let’s run it and verify that
$ docker run --it --rm hello:v3 hello world!
No problem, as expected! Let’s see how the size is.
$ 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
The image size is exactly the same as the second method. And look at the contents of 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
also has only one executable
hello
file, perfect!It’s basically the same as the second final image, but we’ve simplified the process to only a
Dockerfile
and run one command, so I don't need to go through all the obscure shell
and makefile
.Work is perfectly done
So far, my teammates thought it was perfect and gave me a lot of praise! However, every time I write such a
Dockerfile
, I still feel quite annoying, so I got to know about a tool. let me show it~# install it first $ go install github.com/zeromicro/go-zero/tools/goctl@latest # write a Dockerfile with one command $ goctl docker -go hello.go
Done! Look at the generated
Dockerfile
.FROM golang:alpine AS builderLABEL stage=gobuilderENV CGO_ENABLED 0 ENV GOOS linuxRUN apk update --no-cache && apk add --no-cache tzdataWORKDIR /buildADD go.mod . ADD go.sum . RUN go mod download COPY . . RUN go build -ldflags="-s -w" -o /app/hello . /hello.go FROM alpineRUN apk update --no-cache && apk add --no-cache ca-certificates COPY --from=builder /usr/share/zoneinfo/America/New_York /usr/share/zoneinfo/America/New_York ENV TZ America/New_YorkWORKDIR /app COPY --from=builder /app/hello /app/helloCMD [". /hello"]
A few the points to understand are.
cgo
is disabled by default
- Removed the debug message
ldflags="-s -w"
to reduce the image size
- Installed
ca-certificates
so that usingTLS
certificates is not a problem
tzdata
is installed in thebuilder
image, and only the required timezone is copied in the final image
- automatically set the local time zone so that we see
New York
time in the container
Let’s look at the size of the image built with this auto-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
It’s slightly larger because we copied
ca-certificates
and tzdata
. Verify that.Let’s see what’s in the image.
$ docker run -it --rm hello:v4 ls -l /app total 832 -rwxr-xr-x 1 root root 851968 Mar 7 08:36 hello
Also only the
hello
executable, and the file size has been reduced from 1281KB to 851KB. run it and see: shell
executable$ docker run --it --rm hello:v4 hello world!
And you can specify the base image as
scratch
when generating the Dockerfile
, so the image is smaller, but you can't log in directly via sh
.$ goctl docker -base scratch -go hello.go
The size is also 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
And look what’s in the mirror
docker
command supports cross building images. If you’re using Apple Silicon chips, you can pass --platform linux/amd64
to build docker images for linux/amd64
.You can even use
upx
to reduce the binary size. Install upx
in builder stage and execute upx /app/hello
after building.$ docker images | grep hello hello v7 491d2cf669c7 16 minutes ago 548kB # amd64 after upx hello v6 0a80622b3310 7 days ago 972kB # amd64 hello v5 d084eed88d88 13 days ago 1.07MB # arm64
Blog Article by:
Ankur Paul