Create a small Docker image for a GoLang binary

Oct 7, 2015  

A benefit with using GoLang is the ease in deploying to production as it does not require dependencies.

The official Docker images for GoLang are large (typically 500MB+). The reason why they are large is because they contain all the toolchain to build a GoLang program. However you don’t need any of this to actually run it as it is a static binary.

In this post we will see how to generate a small Docker image weighing less than 8MB.

All files mentioned in this post are available here.

Let’s start with a basic Go program:

package main

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

func main() {
        now := time.Now()
        tz, _ := time.LoadLocation("Europe/Paris")
        parisTime := now.In(tz)
        fmt.Printf("Local time: %s\nParis time: %s\n", now, parisTime)

        _, err := http.Get("https://golang.org/")
        if err == nil {
                fmt.Println("GoLang website is UP")
        } else {
                fmt.Printf("GoLang website is DOWN\nErr: %s\n", err.Error())
        }
}

Note: This post is written under OSX.

Let’s compile our binary and check its size.

$ go build main.go
$ ls -lh main
-rwxr-xr-x  1 sebest  staff   6,3M  7 oct 13:38 main

The binary is 6.3 MB.

Now let’s create a Docker image using the following Dockerfile:

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

Building the image:

$ docker build -t demo .
Sending build context to Docker daemon  6.57 MB
Step 0 : FROM scratch
 --->
Step 1 : ADD main /
 ---> 52f8caa90021
Removing intermediate container f4b041cde0f6
Step 2 : CMD /main
 ---> Running in b82368bb2f0a
 ---> c625c39ac16e
Removing intermediate container b82368bb2f0a
Successfully built c625c39ac16e

Now let’s try to run this image:

$ docker run demo
exec format error
Error response from daemon: Cannot start container cda57701734998a80a1637e59db64e0737b4cc17d29041f9318b89a29d1af7f3: [8] System error: exec format error

We got an error: [8] System error: exec format error

The reason is that this binary is an OSX binary and not a Linux binary:

$ file main
main: Mach-O 64-bit executable x86_64

So let’s cross-compile our Go program to a Linux binary.

$ CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
$ file main
main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped

We now have a Linux binary, so let’s rebuild our Docker image and run it:

$ docker run demo
panic: time: missing Location in call to Time.In

goroutine 1 [running]:
time.Time.In(0xecda77bef, 0x3d1be4, 0x8732e0, 0x0, 0x0, 0x0, 0x0)
	/usr/local/Cellar/go/1.5.1/libexec/src/time/time.go:803 +0x85
main.main()
	/Users/sebest/work/blog-demo/demo-build-static-golang-binary-and-docker-image/main.go:12 +0x97

We now have a different error. The reason is because the scratch image does not have any zoneinfo for timezones. Let’s copy the zoneinfo folder from OSX.

As the scratch image does not contain anything, we don’t even have access to mkdir. The workaround here is to create a tar.gz image and use the ADD directive in the Dockerfile.

$ tar cfz zoneinfo.tar.gz /usr/share/zoneinfo

Updated Dockerfile:

FROM scratch
ADD zoneinfo.tar.gz /
ADD main /
CMD ["/main"]

Let’s rebuild and try our new Docker image

$ docker build -t demo .
...
$ docker run demo
Local time: 2015-10-07 20:57:48.686910468 +0000 UTC
Paris time: 2015-10-07 22:57:48.686910468 +0200 CEST
GoLang website is DOWN
Err: Get https://golang.org/: x509: failed to load system roots and no roots provided

So now the zoneinfo issue is fixed but we still have an error when doing a HTTPS request. The reason is because the scratch image does not have any SSL CA certificates.

Let’s download the Certificates from http://curl.haxx.se/docs/caextract.html

curl -o ca-certificates.crt https://raw.githubusercontent.com/bagder/ca-bundle/master/ca-bundle.crt

Let’s fix that:

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

Let’s try with this new Docker image

$ docker build -t demo .
...
$ docker run demo
Local time: 2015-10-07 21:08:49.834033002 +0000 UTC
Paris time: 2015-10-07 23:08:49.834033002 +0200 CEST
GoLang website is UP

Everything works fine now so let’s check the size of the Docker image

$ docker images
REPOSITORY                TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
demo                      latest              dfa7313cd338        3 minutes ago       7.075 MB

As you can see, our binary is 6.3MB and the Docker image is less than 7.1MB!

The source code for all of this is available here.