Fast Go Docker Builds
3 min read
We write a lot of Go at pebl, and most of our deployments are done using containers. In the beginning our simplistic
Dockerbuild setup was causing super slow builds. Over time we've been able to get super fast docker builds with just a few changes!
Be mindful of layer ordering for unnecessary invalidation
Utilize Go's built-in build cache
Instead of manually cleaning up the image, utilize multistage docker builds
Pay attention to layers! In general, you can speed up docker builds by placing operations on files that don't change as often (such as
go.mod) at the beginning of the Dockerfile.
FROM golang WORKDIR /build COPY go.mod go.sum . RUN go mod download # ... more build steps ...
Since your dependencies don't change as frequently as code, this will allow docker to reuse this layer for subsequent docker builds. The same idea applies to other languages with package management, so you want to handle your
Gemfile in a similar fashion.
Use Go's Build Cache
Unlike the old days when compilers were primitive and developers had to manage caching their builds manually, newer languages come with compilers that handle most of this for you.
While this simplifies things when building on your host machine, it can be tricky to utilize during docker builds. Each build is isolated by design, meaning that the compiler can't access the files it generated during the last build.
To fix this, you can specify a special cache mount in the
RUN step that contains the docker build:
FROM golang WORKDIR /build COPY go.mod go.sum . RUN go mod download COPY . . RUN --mount=type=cache,target=/root/.cache/go-build \ go build -o foo . # ... more build steps ...
Be mindful of your own setup here as the cache location can be different. You can also configure Go to use your desired cache location by setting the
Minimal Docker Images
At this point your instinct might be to add cleanup steps after the
go build. But the downside is that this adds extra time to your build stage, and it can be quite tricky to ensure the clean up is kept up to date as the build changes over time.
Instead we prefer to use multistage docker builds. The other benefit is that you can choose a super slim base image, keeping the final docker image minimal.
FROM golang as build WORKDIR /build COPY go.mod go.sum . RUN go mod download COPY . . RUN --mount=type=cache,target=/root/.cache/go-build \ go build -o foo . FROM alpine COPY --from=build /build/foo /foo CMD ["/foo"]
You can even look into the
scratch image, which is a specialized docker image with literally nothing in its file system, not even
/bin/sh! But be warned that debugging such an image can be quite tricky, as you won't be able to get even a shell to poke around.