Published on

Practical Go: Dockerize go apps

Authors

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:

main.go

_19
package main
_19
_19
import (
_19
"fmt"
_19
"log"
_19
"net/http"
_19
"time"
_19
)
_19
_19
func now(w http.ResponseWriter, req *http.Request) {
_19
var local, _ = time.LoadLocation("Asia/Jakarta")
_19
fmt.Fprintf(w, "Now : %v\n", time.Now().In(local))
_19
}
_19
_19
func main() {
_19
http.HandleFunc("/now", now)
_19
fmt.Println("server run on 8080")
_19
log.Fatal(http.ListenAndServe(":8080", nil))
_19
}

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


_8
FROM golang:latest
_8
_8
COPY . /go/src/app
_8
WORKDIR /go/src/app
_8
_8
RUN go build -o main .
_8
_8
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:


_1
$ 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:


_1
$ 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.

Dockerfile

_12
FROM golang:latest as builder
_12
_12
COPY . /go/src/app
_12
WORKDIR /go/src/app
_12
_12
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o main .
_12
_12
FROM scratch
_12
_12
COPY --from=builder /go/src/app/main /main
_12
_12
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


_1
$ docker build -t jakarta-now-small .

The result is amazing


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

But after trying to run docker image he got fatal error


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

main.go

_20
package main
_20
_20
import (
_20
"fmt"
_20
"log"
_20
"net/http"
_20
"time"
_20
_ "time/tzdata"
_20
)
_20
_20
func now(w http.ResponseWriter, req *http.Request) {
_20
var local, _ = time.LoadLocation("Asia/Jakarta")
_20
fmt.Fprintf(w, "Now : %v\n", time.Now().In(local))
_20
}
_20
_20
func main() {
_20
http.HandleFunc("/now", now)
_20
fmt.Println("server run on 8080")
_20
log.Fatal(http.ListenAndServe(":8080", nil))
_20
}

Yeah, after rebuilding dockerfile, the application worked properly.

docker
client-test

_2
❯ docker run -p 8080:8080 jakarta-now-small
_2
server run on 8080

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

Reference