BuildKit’s cache mounts can dramatically speed up your builds, but understanding how they work and when to use them is key to unlocking their full potential.
Here’s a quick demo. Imagine we’re building a Go application.
# First build, takes a while
docker build --tag my-go-app:v1 .
# Second build, using a cache mount
docker build --tag my-go-app:v2 --mount type=cache,target=/root/.cache/go-build .
The second build will be significantly faster, even if you haven’t changed your Go code. Let’s break down why.
The core problem BuildKit solves here is the repeated, expensive work of downloading dependencies or compiling code that hasn’t changed. Traditionally, Docker’s layer caching handles this. If a RUN instruction hasn’t changed, its output layer is reused. However, this is a "black box" cache. BuildKit introduces explicit cache mounts, giving you granular control over specific directories that can be persisted between builds.
When you use --mount type=cache,target=/root/.cache/go-build, you’re telling BuildKit: "Hey, there’s a directory, /root/.cache/go-build, that my build process uses for caching Go build artifacts. Please make sure that whatever is in this directory from a previous build is available for the current build, and any changes made to it during this build are saved for future builds."
The magic happens because BuildKit doesn’t just cache the final layer. It understands the state of specific directories. For Go, the GOCACHE environment variable typically points to /root/.cache/go-build. When you run go build, it intelligently uses this cache. If the source code hasn’t changed, and the dependencies haven’t changed, go build can skip the compilation step entirely, pulling pre-compiled objects directly from the cache.
This isn’t limited to Go. Think about Python’s pip or npm for Node.js.
For Python, the cache often resides in ~/.cache/pip. You’d use:
# Dockerfile snippet
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
This allows pip to reuse downloaded wheels and built packages, drastically reducing installation time on subsequent builds where requirements.txt hasn’t changed.
For Node.js, the npm cache is typically in /root/.npm.
# Dockerfile snippet
RUN --mount=type=cache,target=/root/.npm \
npm ci --cache /root/.npm
npm ci is preferred here as it’s designed for CI/CD environments and is generally faster and more deterministic than npm install. The --cache flag explicitly tells npm where to find/store its cache, which we’re then pointing to the BuildKit cache mount.
The key is identifying the specific directories your build tools use for caching. Common ones include:
- Go:
/root/.cache/go-build(forgo build) - Pip:
/root/.cache/pip(forpip install) - npm:
/root/.npm(fornpm ciornpm install) - Yarn:
/root/.cache/yarn(foryarn install) - Maven:
/root/.m2/repository(for Java projects using Maven) - Gradle:
/root/.gradle/caches(for Java projects using Gradle)
The target path in the --mount directive must match the path your tool expects. Often, you’ll set environment variables in your Dockerfile to ensure this.
# Example for Go
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
# Set GOCACHE to the mount target
ENV GOCACHE=/root/.cache/go-build
RUN --mount=type=cache,target=/root/.cache/go-build \
go build -o myapp .
FROM alpine:latest
COPY --from=builder /app/myapp /myapp
CMD ["/myapp"]
Here, we explicitly set GOCACHE to /root/.cache/go-build, which is also the target of our cache mount. When go build runs, it finds its cache in the expected location, and BuildKit ensures that this directory’s contents are preserved between builds.
The type=cache mount is distinct from type=bind or type=tmpfs. A bind mount links a host directory directly into the container, which is useful for development but not for reproducible builds. A tmpfs mount is in-memory and ephemeral, losing its data when the build finishes. A cache mount, however, is managed by BuildKit and persists its contents across different build invocations, even if the build context or Dockerfile changes, as long as the cache key (derived from the mount’s target and potentially other factors) remains the same.
What many people miss is that the cache mount is tied to the specific layer where it’s declared. If you have multiple stages in your Dockerfile and declare the cache mount in an earlier stage, it won’t be available to later stages unless you explicitly copy its contents or re-declare the mount in the later stage. For maximum benefit, declare cache mounts in the stages where the caching tools are actually run.
The next hurdle you’ll likely face is cache invalidation – understanding why a cache mount isn’t being used when you expect it to be.