Step-by-step to learn and optimize Dockerfile
Tags: go, docker, kubernetes, microservices
Published: 2022-03-22 05:40:59

## 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:

```go
package main

func main() {
    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`:

```dockerfile
FROM golang:alpine

WORKDIR /build

COPY hello.go .

RUN go build -o hello hello.go

CMD [". /hello"]
```

To build the image.

```shell
$ docker build -t hello:v1 .
```

Done, let's get a closer look.

```shell
$ 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.

```shell
$ 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!

![](https://oscimg.oschina.net/oscnet/up-093b3eb9f7955b6320773d7784c6001c1c8.png)

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

```shell
$ 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.

```shell
$ 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.

```dockerfile
FROM alpine

WORKDIR /build

COPY hello .

CMD [". /hello"]
```

Try running

```shell
$ 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~

```shell
$ go build -o hello hello.go
```

Then run `docker build -t hello:v2 .`, no problem, take two steps and try.

```shell
$ 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~

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

Rerun `docker build`, finally done, hurry up and run

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

No problem, let's see the content and size.

```shell
$ 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~

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

Wow, 6.61MB, absolutely!

![](https://oscimg.oschina.net/oscnet/up-5cb66e81ff4ec08e88908d75c2a80ae0486.png)

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.

1. compile the `Go` code natively, if it involves `cgo` cross-platform compilation will be more troublesome
2. build the `docker` image with the compiled executable
3. write a `shell` script or `makefile` 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.

```dockerfile
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` 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

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

No problem, as expected! Let's see how the size is.

```shell
$ 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.

```shell
$ 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!

![](https://oscimg.oschina.net/oscnet/up-4deca7cd59b2d276d1b3407111b60d47d19.png)

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`.

## God's work is 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 wrote a tool. let me show it~

```shell
# 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`.

```dockerfile
FROM golang:alpine AS builder

LABEL stage=gobuilder

ENV CGO_ENABLED 0

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/America/New_York /usr/share/zoneinfo/America/New_York
ENV TZ America/New_York

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

CMD [". /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 using `TLS` certificates is not a problem
* `tzdata` is installed in the `builder` 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`.

```shell
$ 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.

![](https://oscimg.oschina.net/oscnet/up-8a548594bf09305dfdfbbfc05a2440c5128.png)

Let's see what's in the image.

```shell
$ 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

```shell
$ 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`.

```shell
$ goctl docker -base scratch -go hello.go
```

The size is also really small:

```shell
$ 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

![](https://oscimg.oschina.net/oscnet/up-e839a1f78588a16e5838d11b58bd17f5781.png)

You can provide the options `--platform linux/amd64` or `--platform linux/arm64` to `docker build` for generating the docker images of different `OS` and `ARCH`.

Okay, okay, no more `Dockerfile`, I'm going to learn new skills~

## Project address

[https://github.com/zeromicro/go-zero](https://github.com/zeromicro/go-zero)