Building Minimal Docker Image for Go Applications

Learn how to build tiny docker image for Go from scratch

ta-ching chen

3 minute read

Hello World

Lets create a simple small application that keeps printing out Hello World!

// main.go
package main

import (
    "fmt"
    "time"
)

func main() {
    for {
        fmt.Println("Hello World!")
        time.Sleep(1 * time.Second)
    }
}

Compile

CGO_ENABLED let the Go binaries link the libraries on the system. To reduce the binary size, CGO_ENABLED is enabled by default for native build. This time we use scratch as our base image, which is a special Docker image with nothing in it (even the libraries), we need to disable the cgo parameter let compiler packages all the libraries application need into the binary.

If you want to do cross compilation, dont forget to add GOOS and GOARCH.

$ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main

Dockerize

Here is all we need in the Dockerfile

FROM scratch
ADD main /
CMD ["/main"]

Build image

$ docker build -t minimal_go_docker_img .
$ docker images
REPOSITORY                        TAG                 IMAGE ID            CREATED             SIZE
minimal_go_docker_img             1.0                 8f836c8b219a        17 minutes ago      1.638 MB

Launch it

$ docker run --rm minimal_go_docker_img

Add SSL Certificates

If your application need to access https service, dont forget to add the ca.crt.

Normally you can find ca-certificates.crt under the /etc/ssl/certs/ at any linux distribution.

FROM scratch
ADD ca-certificates.crt /etc/ssl/certs/
ADD main /
CMD ["/main"]

Use multi-stage builds to improve continuous delivery pipeline (2018/01/22)

Creating a minial docker image, we first need to produce a go binary and put it into scratch image. Its quite complicated and heavy for people to create continuous delivery pipeline.

To solve this problem, Docker release a feature called Multi-stage builds. With this feature we can merged multiple dockerfiles into one and let base image to copy artifacts and outputs from the intermediate image. In this way, we can keep pipeline easy to read and maintain.

Let’s modify the previous dockerfile

# You can different go builder image at https://hub.docker.com/_/golang/
FROM golang:1.9.2 as go-builder
WORKDIR /go
COPY *.go /go/
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main .

FROM scratch
# Use COPY --from=<base_image_name> to copy artifact from intermediate image
# Or you can use COPY --from=0 to copy from the first intermediate image
COPY --from=go-builder /go/main /main
CMD ["/main"]

Tips

  1. FROM <base_image> as <alias>

    • Add alias for intermediate image for readability.
    • No need to modify Dockerfile if base image changed in future.
  2. COPY --from=<base_image_name>

    • Use alias instead of number of image for readability and maintenance.

Build image again

$ docker build -t multi_stage_minimal_go_docker_img .

You should see log outputs like following

Sending build context to Docker daemon  7.168kB
Step 1/7 : FROM golang:onbuild
# Executing 3 build triggers...
Step 1/1 : COPY . /go/src/app
Step 1/1 : RUN go-wrapper download
 ---> Running in 5755303bcf11
+ exec go get -v -d
Step 1/1 : RUN go-wrapper install
 ---> Running in 0670ccd535d4
+ exec go install -v
app
 ---> b28583d2034e
Removing intermediate container 5755303bcf11
Removing intermediate container 0670ccd535d4
Step 2/7 : WORKDIR /go
 ---> 1d43ded752da
Removing intermediate container c1fa3dde7177
Step 3/7 : COPY *.go /go/
 ---> 523fa478842c
Step 4/7 : RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o main .
 ---> Running in 4a8f59c5439c
 ---> 4c54297c9846
Removing intermediate container 4a8f59c5439c
Step 5/7 : FROM scratch
 --->
Step 6/7 : COPY --from=0 /go/main /main
 ---> 6fd3433462a5
Step 7/7 : CMD /main
 ---> Running in adcad4c99620
 ---> 395ea1e62708
Removing intermediate container adcad4c99620
Successfully built 395ea1e62708
Successfully tagged multi_stage_minimal_go_docker_img:latest

Reference

comments powered by Disqus