Technotes for future me

GoLang: Using multi-stage builds to create clean Docker images

The Go programming language is a natural fit for containers because it can compile down to a single statically-linked binary. And if you place that single executable on top of scratch, a distroless image, or a small image like alpine, your final image has a minimal footprint which is great for consumption and reuse.

But the scenario above is predicated on the host machine doing the compilation of the Go source code, and having all the Go build tools and external packages so that it can produce that final executable; which it then copies onto the image.

However, this model is not a good fit for an automated CI/CD pipeline. For an automated pipeline, it is better to have the source code copied into a base container that contains the exact set of build tools, packages, and dependencies that can then faithfully produce the final executable.

The only problem now is that the image produced may have tens or hundreds of megabytes of build tools and cruft sitting inside, while all we need is the final executable.

Luckily, multi-stage builds are able to address this issue. We can use a temporary intermediate image to do the build/compilation/linking, and then take the result and copy it into a clean final image that only contains the final executable.

Multi-stage image build

The key to this work is in the Dockerfile. See my example below from my golang-memtest project on github.

The first FROM statement uses an alias as builder name so we can reference it later in the file. This is the intermediate layer where the Go build tools and compilation happen. It is about 350Mb.

The second FROM is a clean version of alpine, where we simply COPY –from=builder to get the executable from the intermediate layer. This final image is the one presented to end users of the service, and only weighs 13Mb.

# builder image
FROM golang:1.15.2-alpine3.12 as builder
RUN mkdir /build
ADD *.go /build/
WORKDIR /build
RUN CGO_ENABLED=0 GOOS=linux go build -a -o webserver .

# generate clean, final image for end users
FROM alpine:3.12

COPY --from=builder /build/webserver .
COPY image.jpg .
COPY index.html .

# executable
ENTRYPOINT [ "./webserver" ]

# http server listens on port 80.


Last updated on 31 Jan 2021
Published on 29 Sep 2020
Edit on GitHub