2 min read

Faster multi-arch container builds: golang edition

Faster multi-arch container builds: golang edition
Photo by Shaah Shahidh / Unsplash

Following on from my article on faster multi-arch container builds using rust, today I'll be applying the same pattern to a go service. For the service I've used, my GitHub multi-arch build times dropped down from roughly 6 minutes to 1:30!

If you're not familiar with how buildx handles multi-arch builds, have a quick skim over the rust version of this blog so this makes sense.

With go, cross-compilation is even easier than rust. If you are targeting Linux you don't need to depend on glibc or musl as go takes advantage of the Linux system call API being stable and uses it directly rather than calling through a libc-style library [1]. For a simple service without native C dependencies, it's super easy to whittle things down to a single, focused binary, and we don't need to do wild things with LLVM now either.

# syntax=docker/dockerfile:1

#
# Explicitly use the current platform as our build platform, so buildx
# doesn't emulate the target
#
FROM --platform=${BUILDPLATFORM} golang:1.22 AS builder

# Builds deps separately for caching purposes
COPY go.mod go.sum ./
RUN go mod download

# Build app
# CGO_ENABLED disables c-interop and forces a static link. This
# is great if you can get away with it!
# The linker flags "-s -w" strip symbols and debug to make the image smaller.
# You may or may not want to use these
COPY *.go ./
ARG TARGETPLATFORM
RUN CGO_ENABLED=0 GOOS=linux GOARCH="${TARGETPLATFORM#*/}" go build \
    -ldflags="-s -w" -o /app/pass-api

#
# Release stage - scratch gives us an empty docker base so we have nothing
# in it apart from our app!
#
FROM scratch

# Set the workdir and add our app
WORKDIR /app
COPY --from=builder /app/pass-api /app/pass-api

CMD ["./pass-api"]

In comparison to the rust version, there's no system packages to install at all, and we can convert the docker buildx style TARGETPLATFORM easily with a bash substitution inline. And, we get a nice, tiny ~25 MiB image out at the end too.

Once again, there's a complete worked Dockerfile example on github and associated GitHub workflow.


  1. libc-style libraries are used on Linux to provide user-space access to the kernel. They wrap up the kernel API itself in a friendly C-API. Glibc is the most well-known, whilst musl is a smaller, typically-statically-linked variant popular for optimising docker image size. Going a little deeper, the kernel API is invoked by putting magic numbers into particular registers and firing off an IRQ to jump back into kernel space. This is naturally not the most ergonomic thing to do, which is why libc exists. ↩︎