Docker & ContainersV · Dockerfiles & BuildKitBuildKit
BuildKit — the modern builder
What you'll learn
- Explain what BuildKit is and where the build engine runs for each buildx driver
- Predict where a built image lands, and why `docker run` sometimes cannot find it
- Use the `# syntax=` directive to move the Dockerfile frontend independently of the daemon
- Read a BuildKit build graph and explain why stages run in parallel
- Bound the build cache on disk before it bounds the host for you
Prerequisites
Verified against Docker Engine 29.x · Docker Engine 28.x · Docker Compose 2.x · containerd 2.x · runc 1.2.x · BuildKit 0.20+ · Linux kernel 5.15+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-12
BuildKit is the modern builder for Docker images. It is the default builder for Docker Engine and Docker Desktop users. It produces better images faster than the legacy builder and unlocks features the legacy builder cannot support.
It is also a genuinely separate piece of software from dockerd,
with its own graph model, its own cache store and — depending on
how you invoke it — its own process or even its own host. Nearly
every “BuildKit is behaving strangely” ticket is really a question
about which BuildKit you were talking to.
Enabling BuildKit
For Docker 23+, BuildKit is the default. To verify:
docker buildx version
docker info | grep -i buildkit
To force BuildKit on older Docker:
DOCKER_BUILDKIT=1 docker build .
Or via daemon config:
{
"features": {
"buildkit": true
}
}
The classic builder still exists on Engine 28.x and can be selected
with DOCKER_BUILDKIT=0, but Docker’s deprecation page is
unambiguous: v23.0 “marks the beginning of the deprecation cycle of
the classic (‘legacy’) builder for Linux images.” No removal date is
published, which makes it exactly the kind of dependency that is
fine until the release where it is not. If a build in your estate
still needs DOCKER_BUILDKIT=0, that is a migration ticket, not a
setting.
What BuildKit actually is
This is also why the build output numbers steps as #7 [3/5]
rather than by Dockerfile line number: the number is a vertex in
the graph, and the graph is not the file.
Where BuildKit runs: the driver decides
docker buildx can drive four different builders, and the choice
changes where the engine lives, where the cache lives, and where a
finished image lands.
| Driver | Where the engine runs | Multi-platform | Cache export | Image lands where |
|---|---|---|---|---|
docker (default) | Inside dockerd | No | Limited | Local image store, automatically |
docker-container | A moby/buildkit container on the host | Yes | Full | Nowhere, until --load or --push |
kubernetes | Pods in a cluster | Yes | Full | Nowhere, until --load or --push |
remote | An already-running buildkitd you point at | Yes | Full | Nowhere, until --load or --push |
The default docker driver, in Docker’s words, “prioritizes
simplicity and ease of use” — and it buys that simplicity by
embedding BuildKit in the daemon, which is why it cannot build
multi-platform images and why its cache export options are
restricted.
docker buildx ls
docker buildx inspect --bootstrap$ docker buildx lsNAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
multiarch* docker-container
\_ multiarch0 \_ unix:///var/run/docker.sock running v0.20.2 linux/amd64, linux/arm64, linux/arm/v7
default docker
\_ default \_ default running v0.20.2 linux/amd64Illustrative output
The asterisk marks the selected builder. That one character is the answer to a surprising share of build mysteries.
The # syntax= directive
This is the most operationally useful BuildKit feature that most people never turn on.
# syntax=docker/dockerfile:1
FROM alpine:3.21
RUN --mount=type=cache,target=/var/cache/apk apk add curl
The directive names a frontend image. BuildKit pulls it and uses it to parse the Dockerfile, instead of using whatever frontend is compiled into the daemon. The consequence, in Docker’s words, is that you “use the latest features without updating the Docker daemon” and “automatically get bug fixes without updating the Docker daemon.”
For a fleet, that inverts a painful dependency. Dockerfile features
— --mount=type=secret, COPY --link, COPY --parents,
heredocs — normally arrive with an Engine upgrade, which is a
change-controlled, container-restarting event. With the syntax
directive they arrive with a pull of a small frontend image, which
disturbs nothing that is running.
Two channels are published:
docker/dockerfile:1— stable, tracks the latest 1.x.x.docker/dockerfile:1-labs— the same plus experimental features.
The BuildKit cache model
BuildKit’s cache is content-addressable. Each build step has an identifier derived from:
- The instruction’s text.
- The contents of files referenced by the instruction.
- The cache key passed to the step.
- The build arguments.
If two builds have identical inputs, they share cached output. This is true across machines: a CI runner’s cache can be pushed to a registry and pulled by a developer’s machine.
# Push the cache to a registry after a build
docker buildx build --push --cache-to=type=registry,ref=myorg/myapp:cache,mode=max .
# Pull the cache for a new build
docker buildx build --cache-from=type=registry,ref=myorg/myapp:cache .
The mode=max cache stores every layer, not just the final one.
This makes CI caches reusable for arbitrary branches.
The next lesson takes the key rules apart instruction by
instruction; what matters here is that this cache belongs to the
builder, not to the daemon and not to the image store.
docker buildx rm deletes it. docker system prune does not
reach into a docker-container builder to clear it.
buildx — the modern CLI
docker buildx is the CLI for BuildKit. It supports:
- Multi-platform builds (
--platform linux/amd64,linux/arm64). - Multiple builders (
docker buildx create --name ci). - Inline cache output (
--cache-to=type=inline). - Secrets (
--secret id=key,src=./key.pem). - SSH forwarding (
--ssh default).
# Create a builder that supports multi-platform
docker buildx create --use --name multiarch
# Multi-platform build
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag myorg/myapp:1.0.0 \
--push \
.
Inspect the active builder:
docker buildx inspect --bootstrap
docker buildx create defaults to the docker-container driver,
which is why the command above both enables multi-platform builds
and silently changes where your images land. Read the previous
callout again before running it on a shared build host.
Build secrets
BuildKit can pass secrets to RUN steps without baking them into
layers:
RUN --mount=type=secret,id=github_token \
curl -H "Authorization: Bearer $(cat /run/secrets/github_token)" \
https://api.example.com/v1/artifacts
docker buildx build --secret id=github_token,src=./token .
The secret is mounted at /run/secrets/<id> during the build step.
It is not present in any layer. After the step, the secret is
gone.
SSH mount
For builds that need SSH access (e.g. pulling private Go modules):
RUN --mount=type=ssh \
go mod download
docker buildx build --ssh default .
The host’s SSH agent is forwarded to the build container for the duration of the step.
Inline cache output
For builds whose cache should travel with the image:
docker buildx build --cache-to=type=inline .
The cache metadata is embedded in the image manifest. When a CI runner pulls the image, it can use the cache for the next build without a separate cache store.
Verifying the build engine you are using
Three checks that can each fail, in the order worth running them:
docker buildx ls
docker buildx inspect --bootstrap | grep -E 'Name|Driver|Buildkit|Platforms'
docker info --format '{{.DriverStatus}}'$ docker buildx inspect --bootstrapName: default
Driver: docker
Last Activity: 2026-08-12 09:14:03 +0000 UTC
Nodes:
Name: default
Endpoint: default
Status: running
BuildKit version: v0.20.2
Platforms: linux/amd64, linux/386Illustrative output
If Driver says docker, images go straight into the local image
store. If it says anything else, they do not unless you ask.
Knowledge check
Knowledge check · 6 questions
Q1. BuildKit's main advantages over the classic builder include:
Q2. BuildKit is enabled by default on Docker Engine 28.x.
Q3. `docker buildx build -t app:dev .` reports success, but `docker run app:dev` says the image cannot be found and tries to pull it. What is the most likely cause?
Q4. What does `# syntax=docker/dockerfile:1` at the top of a Dockerfile do?
Q5. Which are direct consequences of BuildKit modelling the build as a DAG rather than a line-by-line script? Select all that apply.
Q6. Which Dockerfile directive enables BuildKit cache mounts for pip?
Passing score: 75%. Answers are checked in this browser.