Skip to main content
RunBook Academy

Docker & ContainersV · Dockerfiles & BuildKitBuildKit

BuildKit — the modern builder

Intermediate⏱ ~28 mindocker

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

Not yet marked complete on this device.

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.

DriverWhere the engine runsMulti-platformCache exportImage lands where
docker (default)Inside dockerdNoLimitedLocal image store, automatically
docker-containerA moby/buildkit container on the hostYesFullNowhere, until --load or --push
kubernetesPods in a clusterYesFullNowhere, until --load or --push
remoteAn already-running buildkitd you point atYesFullNowhere, 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.

Read-only / Safewhich builder
docker buildx ls
docker buildx inspect --bootstrap
Read-only / Safebuildx ls
$ docker buildx ls
NAME/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/amd64

Illustrative 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:

Read-only / Safeverify
docker buildx ls
docker buildx inspect --bootstrap | grep -E 'Name|Driver|Buildkit|Platforms'
docker info --format '{{.DriverStatus}}'
Read-only / Safeinspect
$ docker buildx inspect --bootstrap
Name:          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/386

Illustrative 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

  1. Q1. BuildKit's main advantages over the classic builder include:

  2. Q2. BuildKit is enabled by default on Docker Engine 28.x.

  3. 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?

  4. Q4. What does `# syntax=docker/dockerfile:1` at the top of a Dockerfile do?

  5. Q5. Which are direct consequences of BuildKit modelling the build as a DAG rather than a line-by-line script? Select all that apply.

  6. Q6. Which Dockerfile directive enables BuildKit cache mounts for pip?

Passing score: 75%. Answers are checked in this browser.