Practical Go: Dockerize go apps

📚 4 min read Tweet this post

Once upon a time, in a land far, far away, there was a Go developer named Upix. Upix had a simple Go app that he wanted to turn into a Docker container. He build a simple ReST API to show the current date in UTC+7.

Here is the code:

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"
)

func now(w http.ResponseWriter, req *http.Request) {
	var local, _ = time.LoadLocation("Asia/Jakarta")
	fmt.Fprintf(w, "Now : %v\n", time.Now().In(local))
}

func main() {
	http.HandleFunc("/now", now)
	fmt.Println("server run on 8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Upix began by creating a new file in his Go app directory called “Dockerfile”. In this file, he wrote the following:

FROM golang:latest

COPY . /go/src/app
WORKDIR /go/src/app

RUN go build -o main .

CMD ["/go/src/app/main"]

This Dockerfile told Docker to use the latest version of the official Go image as the base image for the container. It then copied all the files in the current directory (the Go app) into the container, set the working directory to the app directory, and ran the go build command to build the app. Finally, it set the default command for the container to run the built app.

Next, Upix opened a terminal and navigated to his Go app directory. Then, he ran the following command to build the Docker image:

$ docker build -t jakarta-now .

This command told Docker to build an image with the name “jakarta-now” based on the instructions in the Dockerfile in the current directory (denoted by the .).

After a few minutes, the Docker image was built successfully. Upix was now ready to run the container. He ran the following command:

$ docker run -p 8080:8080 jakarta-now

This command told Docker to run the “jakarta-now” image as a container, and to map the container’s port 8080 to the host’s port 8080.

Upix opened a web browser and navigated to “localhost:8080/now”, and to his delight, he saw the “Now : 2023-01-08 09:31:01.616292115 +0700 WIB” message displayed on the page!

Upix was happy with the result, but seeing the docker image size is big he is wondering how to make it smaller and efficient docker image so if there any update the download size is small.

After doing some research he founds workaround to solve the issue:

  • Make binary output smaller
  • Use minimal base image in docker images such as alpine or scratch. He try using scratch due the current application that he built is very simple.
  • Use multiple build stage in dockerfile

Upix modified the Dockerfile to reduce image size.

FROM golang:latest as builder

COPY . /go/src/app
WORKDIR /go/src/app

RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o main .

FROM scratch

COPY --from=builder /go/src/app/main /main

CMD ["/main"]

He use multiple build stage in docker image, as builder using golang:latest and runner using raw scratch. Another trick is using -ldflags="-s -w" when build go apps to reduce output binary, he can make smaller image by using upx but he avoid to use it because building image takes more time if using upx. After the build application completed the binary location in the builder with path /go/src/app/main. The final step is copy from builder to the runner image.

Upix create the docker image with another tag jakarta-now-small

$ docker build -t jakarta-now-small .

The result is amazing

 docker image list
REPOSITORY                                  TAG                              IMAGE ID       CREATED          SIZE
jakarta-now-small                           latest                           ff2c7caebd56   34 seconds ago   4.89MB
jakarta-now                                 latest                           07d16a449e6a   17 minutes ago   1.01GB

But after trying to run docker image he got fatal error

❯ docker run -p 8080:8080 jakarta-now-small
server run on 8080
2023/01/08 02:48:55 http: panic serving 172.17.0.1:54118: time: missing Location in call to Time.In
goroutine 18 [running]:
net/http.(*conn).serve.func1()
        /usr/local/go/src/net/http/server.go:1850 +0xbf
        2023/01/08 02:48:55 http: panic serving 172.17.0.1:54118: time: missing Location in call to Time.In
goroutine 18 [running]:
net/http.(*conn).serve.func1()
        /usr/local/go/src/net/http/server.go:1850 +0xbf
panic({0x626340, 0x6db150})
        /usr/local/go/src/runtime/panic.go:890 +0x262
time.Time.In(...)
        /usr/local/go/src/time/time.go:1122
main.now({0x6ddca8?, 0xc0001200e0?}, 0x4a44d3?)
        /go/src/app/main.go:12 +0x15a
net/http.HandlerFunc.ServeHTTP(0xc000093af0?, {0x6ddca8?, 0xc0001200e0?}, 0x0?)
        /usr/local/go/src/net/http/server.go:2109 +0x2f
net/http.(*ServeMux).ServeHTTP(0x0?, {0x6ddca8, 0xc0001200e0}, 0xc00011c000)
        /usr/local/go/src/net/http/server.go:2487 +0x149
net/http.serverHandler.ServeHTTP({0xc0000a0ea0?}, {0x6ddca8, 0xc0001200e0}, 0xc00011c000)
        /usr/local/go/src/net/http/server.go:2947 +0x30c
net/http.(*conn).serve(0xc0000b6aa0, {0x6de080, 0xc0000a0db0})
        /usr/local/go/src/net/http/server.go:1991 +0x607
created by net/http.(*Server).Serve
        /usr/local/go/src/net/http/server.go:3102 +0x4db

Ah, he forgot that in scratch image don’t have a time zone data. If he using alpine it simply run apk add tzdata in the Dockerfile runner, but unfortunately he use scratch base image.

After reading and ask his mentor, he found out that starting from Go 1.15, time/tzdata is available in go standard library, he just need to import the package in the main package. So the final main.go file become like this

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"
	_ "time/tzdata"
)

func now(w http.ResponseWriter, req *http.Request) {
	var local, _ = time.LoadLocation("Asia/Jakarta")
	fmt.Fprintf(w, "Now : %v\n", time.Now().In(local))
}

func main() {
	http.HandleFunc("/now", now)
	fmt.Println("server run on 8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Yeah, after rebuilding dockerfile, the application worked properly.

 docker run -p 8080:8080 jakarta-now-small
server run on 8080
 curl localhost:8080/now
Now : 2023-01-08 10:00:34.281244452 +0700 WIB

And so, Upix lived happily ever after, having successfully created a Docker container for his simple “jakarta-now” Go app. The end.

programming go practical