BuildKit’s inline cache in registry images means that the build cache metadata is stored directly within the image manifest itself, rather than in a separate cache backend.

Here’s what that looks like in practice. Imagine you have a Dockerfile that installs some dependencies:

FROM alpine:latest
RUN apk update && apk add --no-cache curl
RUN curl -o /app/mydata https://example.com/data.zip
RUN unzip /app/mydata -d /app

When you build this with BuildKit and push it to a registry, the cache layers are bundled with the image. If you later pull that image and start a new build based on it, BuildKit can leverage these cached layers directly from the registry.

Let’s say you’re building an image with a multi-stage build and want to cache intermediate layers efficiently. You can configure BuildKit to use the registry as its primary cache source.

# syntax=docker/dockerfile:1
FROM golang:1.20-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /app/myprogram

FROM alpine:latest
COPY --from=builder /app/myprogram /app/myprogram
CMD ["/app/myprogram"]

If you build this and push it to a registry like myregistry.com/myproject/myapp:latest, the builder stage’s downloaded modules and the compiled program are part of the image’s layers. When you pull myregistry.com/myproject/myapp:latest and then run docker build . --tag myregistry.com/myproject/myapp:v2 --cache-from myregistry.com/myproject/myapp:latest, BuildKit will inspect the manifest of myregistry.com/myproject/myapp:latest. It finds the layers corresponding to the builder stage, sees that go mod download and go build haven’t changed, and reuses those layers without re-executing the commands.

The core problem this solves is the overhead and complexity of managing separate cache backends for distributed build environments. Traditionally, you might set up a dedicated Redis or local filesystem cache, which requires separate provisioning, maintenance, and access control. With inline caching, the cache is intrinsically linked to the image you’re building and distributing. This is particularly powerful for CI/CD pipelines where build agents come and go; they can pull an image, use its embedded cache for subsequent builds, and push a new image with its own updated cache, all without a shared, external cache infrastructure.

Internally, BuildKit uses the OCI image specification. When you build an image with caching enabled and push it, BuildKit annotates the image manifest and its layers with specific metadata. This metadata essentially says, "This layer was generated by running command X with these inputs, and it produced these outputs." When another BuildKit daemon pulls this image and uses it as a cache source (--cache-from), it parses these annotations. If it encounters a RUN or COPY instruction in a new Dockerfile that matches a cached layer’s annotation, it skips the execution and directly uses the pre-existing layer from the registry.

The exact levers you control are primarily through the DOCKER_BUILDKIT=1 environment variable (or the equivalent setting in your Docker daemon configuration) and the --cache-from flag during the build. The --cache-from flag tells BuildKit which images to consult for cache. When you specify an image that has been built and pushed with BuildKit’s inline cache enabled, you’re telling it to look for cache metadata within that image’s layers. You can list multiple --cache-from images, and BuildKit will prioritize the cache hits from them in the order provided.

The surprising efficiency comes from how BuildKit handles cache invalidation and layer reuse. It doesn’t just look at the command itself; it also considers the inputs to that command. For a RUN command, this includes the content of all files in the build context that the command might depend on. For a COPY command, it’s the content of the files being copied. BuildKit calculates a content-addressable hash for these inputs. If the hash matches a previously cached layer’s input hash, even if the command string is slightly different (e.g., added comments), the cache is reused. This granular approach to cache invalidation is a key differentiator from older Docker build systems.

The next step in optimizing builds after leveraging inline cache is exploring BuildKit’s distributed cache sharing capabilities, which can synchronize cache state across multiple build nodes even when the cache isn’t directly embedded in the image.

Want structured learning?

Take the full Buildkit course →