Building-a-Golang-application-Docker-image

We'll find a way; we always have.

After we have written the application after several months of hard work, how to deploy it? Let’s use a simple example of Hello Worldto learn.

The project structure is as follows:

1
2
3
.
├── go.mod
└── hello.go

The code content of hello.gois as follows:

1
2
3
4
package main
func main() {
println("hello world!")
}

In order to keep up with the trend, we choose to use Docker deployment here.

First attempt.

For convenience, we are going to put all the content into Docker for compilation, and after some research, we get the following Dockerfile file:

1
2
3
4
5
FROM golang:alpine
WORKDIR /build
COPY hello.go .
RUN go build -o hello hello.go
CMD ["./hello"]

Next start building.

1
2
3
4
5
6
7
8
9
$ docker build -t hello:v1 .
$ 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

# try to run it
$ docker run -it --rm hello:v1
hello world!

It runs successfully, and then we look at the size of the image.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ docker images | grep hello
hello v1 2783ee221014 44 minutes ago 314MB
````

Shocked me, the whole image actually has 314MB, just docker build, what happened?

Although it can be run, the size of this image is too scary, we just simply printed a line of hello world, and the size of the image is more than 300 MB, which is too unreasonable and needs to be optimized.


### Second attempt.
After looking for the information, I found that the base image we used was too large.

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

A friend told me that I can compile the code first, and then copy it in, so I don’t need that huge base image, but it’s easy to say, I still spent some time learning, and finally the Dockerfile looks like this:

1
2
3
4
FROM alpine
WORKDIR /build
COPY hello .
CMD ["./hello"]

Let’s rebuild the image:

1
2
3
4
5
6
7
$ 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

Oh hoo, wrong report. The prompt hello cannot be found, so I forgot to compile hello.go first and execute it again.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ go build -o hello hello.go
$ docker run -it --rm hello:v2
standard_init_linux.go:228: exec user process caused: exec format error
````

Whoops, failed again.

Well, the format is wrong, it turns out that our development machine is not linux, Don’t give up, let’s do it again.

```shell
$ GOOS=linux go build -o hello hello.go
$ docker build -t hello:v2 .
# ...
Successfully

Finally, the build is successful, let’s try it out.

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

No problem, let’s take a look at the content and size.

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

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

Wow, it’s only 6.61MB this time, which is OK!

Third attempt.

Although the above image can be successfully built, there are still some shortcomings. It is not a multi-stage build.

We need to be able to build a docker image from Go code, which is divided into three steps:

  • Compile Go code natively, if it involves cgo the cross-platform compilation will be more troublesome.
  • Build a docker image with the compiled executable.
  • Write a shell script or makefile to get these steps in one command.
    Multi-stage builds are all about putting it all into one Dockerfile, no source code leaks, no scripting for cross-platform compilation, and a minimal image.

Loving to learn and striving for perfection, I ended up writing the following Dockerfile.

1
2
3
4
5
6
7
8
9
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 with building a builder image in which the executable hellois compiled.

The part starting with the second FROM is to copy the executable hello from the first image, and use the smallest possible base image alpine to ensure that the final image is as small as possible.

As for why you don’t use a smaller scratch, it’s because there’s really nothing in scratch, and there is no chance to take a look if there is a problem, and alpine is only 5MB, which is good for our service will not have much impact.

Let’s run it first to verify:

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

No problem, as expected! See what the size looks like:

1
2
3
4
$ 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 size of the image built by the second method is exactly the same. Take a look at the contents of the mirror:

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

Also, only one executable hello file builds perfectly!

https://blog.devgenius.io/tutorial-building-a-golang-application-docker-image-78e36d437c70