Multi-stage Docker Builds in Golang with Go Modules

A tutorial on multi-stage Docker builds with Go using Go Modules and how to reduce the Docker image size by hundreds of megabytes

Niraj Fonseka
Level Up Coding

--

In this article I will give you a quick walkthrough on how to create smaller Docker images for Go applications that use Go modules. You can find all the code for this article in this repository.

In a traditional Docker build for a Go app, it’s pretty common to use the Golang image. While this is perfectly fine, these images tend to be quite large due to the size of the Golang image itself. Let’s look at an example to see how we can reduce our image size by hundreds of megabytes.

In my example, I’ll be using Go Modules for dependency management. If you are unfamiliar with Go Modules, feel free to check out my previous article on Go Modules.

Let’s consider this is the app we would like to dockerize.

package main

import (
"fmt"

randomdata "github.com/Pallinder/go-randomdata"
)

func main() {
fmt.Println("Running the TestApp")
fmt.Println(randomdata.SillyName())
}

And this is our Dockerfile:

FROM golang:1.11.0-stretch 

COPY . /SingleStage
WORKDIR /SingleStage

ENV GO111MODULE=on

RUN CGO_ENABLED=0 GOOS=linux go build -o SingleStage

CMD ["./SingleStage"]

As you can see, we are using golang:1.11 image as the base image to build our app and that GOPATH is not used in the build. That’s because Go 1.11 allows you to build applications outside of the GOPATH.

Now let’s build the app.

docker build -t singlestagebuild .

If we run the docker images command and look at the size of the image we can see that even for a very simple app like this the size is almost 800MB.

Now let’s see how we can reduce the size of the image using a multi-stage Docker build. I’ll still be using the previous Go app for this demonstration as well.

This is our new Dockerfile.

#first stage - builderFROM golang:1.11.0-stretch as builderCOPY . /MultiStageWORKDIR /MultiStageENV GO111MODULE=onRUN CGO_ENABLED=0 GOOS=linux go build -o MultiStage#second stageFROM alpine:latestWORKDIR /root/COPY --from=builder /MultiStage .CMD ["./Multistage"]

In the first stage, we are using the same image that we used in the first example. The only difference is instead of CMD [“./Multistage”], I’m starting up the second stage using an Alpine image and set the working directory to /root/. Then I’m copying all the contents from /MultiStage directory to our builder image (where the binary for the Multistage also exists).

Let’s build and see how big our image is.

docker build -t multistage .

There we go! Reducing down to 8.15MB from 781MB is a huge size difference.

The Alpine image that we use as our second stage is an extremely lightweight distribution of Linux. So you will have to be careful because you need to ensure that any of your Go libraries are not using any Linux internal libraries / files that are not included in the Alpine image, and if they do, you must manually add them. Let’s take a look at another example and see this in action.

In this example, we are trying to get the time in Berlin by using the LoadLocation() function in the time package.

package main

import (
"fmt"
"time"
)

func main() {

location, err := time.LoadLocation("Europe/Berlin")
if err != nil {
fmt.Println(err)
}

t := time.Now().In(location)

fmt.Println("Time in Berlin:", t.Format("02.01.2006 15:04"))
}

Now let’s dockerize it and run it.

docker build -t loadlocation .

docker run loadlocation

Uh oh…

Whenever we call the LoadLocation() function in the time package it looks for the “Time Zone Database” (zoneinfo.zip ) which maintains a list of time zone information from locations around the world. Since the alpine image does not have this file we will have to inject it into the image manually.

RUN apk add --no-cache tzdata

You may need to inject CA root certs into the Alpine image as well. you can do this by using apk.

RUN apk --update add ca-certificates

Or copying the ca-certs from the builder image (first stage)

COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

Now let’s look at the final Dockerfile.

#first stage - builderFROM golang:1.11.0-stretch as builderCOPY . /LoadLocationWORKDIR /LoadLocationENV GO111MODULE=onRUN CGO_ENABLED=0 GOOS=linux go build -o LoadLocation#second stageFROM alpine:latestWORKDIR /root/RUN apk add --no-cache tzdataCOPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/COPY --from=builder /LoadLocation .CMD ["./LoadLocation"]

Finally, let’s build and run the image.

Perfect!

I hope this article will help you optimize your docker builds and create more secure (reduced attack surface), smaller (8.15MB vs 781MB), and cleaner images.

--

--