Multistage Docker Builds For Go Services

One thing I like about using Go for services is that it can have a much smaller footprint than other languages. This is particularly useful when building Docker images to run since a Docker container contains everything needed to run a service.

But images sizes can grow quickly. As Go code gets more complicated you can and up with a large number of dependencies and tests. Sometimes additional tools are needed as part of the build process such as for code generation. I also like to have various tools available when debugging build problems. But you can have all of that and still end up with small image by using multistage builds in Docker.

Multistage builds were added to Docker a couple of years ago and work very well. I will refer interested parties to the official documentation, but one issue I have encountered as I have used multistage builds is that sometimes the Go executable I create in a penultimate stage does not execute properly in the image created in the final stage. Typically the reason for this is because I use Ubuntu based images for building (Ubuntu is the Linux flavor I’m most comfortable using) and prefer Alpine based images for the final package to reduce size. So this is perhaps a problem of my own making, but I have found solutions to the common errors I see.

FROM golang:latest as builder 

# Generic code to copy, compile and run unit tests
WORKDIR /src
COPY src/ ./
RUN go test -v


FROM alpine:latest

WORKDIR /root/

# If you don't adde the certificates you get an error like:
# x509: failed to load system roots and no roots provided
RUN apk --no-cache add ca-certificates

# Because musl and glibc are compatible we can link and fix
# the missing dependencies.  Without this we get an error like:
# sh: ./main: not found
RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2

# Copy the go exec from the builder
COPY --from=builder /go/bin/main .

CMD [ "./main" ]

The above Dockerfile is a generic multistage build of a “main” go application. Here are couple things of note:
– The “RUN apk –no-cache add ca-certificates” snippet is necessary to avoid “x509” errors.
– The “RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2” snippet is a bit of a hack to get around the glibc vs musl in Ubuntu vs Alpine. Without it, you get unhelpful, difficult-to-debug errors like “sh: ./main: not found”.

An alternative to the library linking above is to instead set flags when compiling in Ubuntu like “CGO_ENABLED=0 GOOS=linux”.

When using multistage Docker builds for Go services, I’ve found the image for the first stage to be several hundred megabytes, but the image produced by the final stage will often be around a dozen megabytes. You can’t even fit a JVM in an image that small let alone a Java based service.

Author: Nathan

I like to do stuff.

Leave a Reply

Your email address will not be published. Required fields are marked *