Chapter 5: Building Well
Topic 27

BuildKit — The Modern Builder

BuilderBuildKit

BuildKit is the engine that turns a Dockerfile into an image, and on current Docker it is the default — the old line-by-line builder is gone unless you deliberately ask for it back. Instead of marching through the file top to bottom, it parses the whole Dockerfile into a dependency graph, runs independent steps at the same time, and caches each step by the content of its inputs rather than by its position in the file.

Everything the rest of this chapter relies on exists because BuildKit is doing the work: --mount=type=secret, --mount=type=cache, and parallel multi-stage builds are all features the legacy builder never had. If you have written a Dockerfile in the last few years, BuildKit almost certainly built it, whether you invoked docker build or docker buildx build.

The Legacy Builder vs BuildKit

The old builder executed instructions strictly top to bottom, one throwaway container per step, with a linear cache keyed on instruction order. Change line 4 and every line below it rebuilt, regardless of whether line 9 actually depended on line 4. BuildKit instead builds a graph of the file, works out which steps depend on which, and runs unrelated branches concurrently — which is precisely why a multi-stage build with two independent stages builds both at the same time instead of one after the other.

The cache changes with it. BuildKit keys each step on the actual content of its inputs, so reordering an instruction that nothing below depends on no longer invalidates the rest of the file. The full cache mechanics land in topic 30 with the cache mount; the point here is that "instruction order is everything" was true of the old builder and is only partly true of BuildKit.

Legacy builder vs BuildKit
Legacy builder
Executes instructions sequentially, top to bottom, with a position-based cache. No --mount=type=cache, no build secrets — change one line and everything below it rebuilds.
BuildKit
Parses the file into a dependency graph and runs independent branches in parallel, caches each step by the content of its inputs, and adds --mount=type=cache and --mount=type=secret.

How It's Enabled

On current Docker Desktop and Docker Engine, BuildKit is the default builder — you get it from a plain docker build . with nothing extra to set. On an older daemon you opt in by exporting DOCKER_BUILDKIT=1 in front of the build, or by switching to docker buildx build, which is BuildKit's full-featured front door and the command the production chapters assume. There is no separate install on a recent Docker; the engine ships with BuildKit built in.

The # syntax Frontend Line

A Dockerfile whose first line is # syntax=docker/dockerfile:1 tells BuildKit to fetch that Dockerfile frontend at build time, so the build gets a current instruction set — including the --mount family — regardless of how old the daemon underneath is. This one comment is the prerequisite for cache mounts and build secrets later in the chapter; without it, an older daemon parses RUN --mount=type=secret as a syntax error and the build dies before it starts.

Driftwood's Dockerfile opens with the frontend directive
# syntax=docker/dockerfile:1
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "driftwood.wsgi"]

The directive pins the frontend — the parser that understands the Dockerfile — to the dockerfile:1 channel, which stays current with stable instruction additions. It does not pin the base image; that is FROM's job, and digest-pinning the base is topic 32's subject.

Parallelism and Smarter Caching

Because BuildKit knows the dependency graph, it parallelizes work that has no ordering relationship and caches by input content. Split genuinely independent setup into separate stages and BuildKit builds them concurrently rather than serializing a long RUN chain. The content cache also means an unrelated edit no longer cascades: touch a comment near the top and the steps below it stay cached as long as their own inputs are unchanged.

buildx as the Interface

docker buildx is the CLI plugin that exposes BuildKit's full surface: named builders, cache export and import, build secrets, and multi-platform output via --platform (previewed in topic 32, full treatment in Chapter 9). Plain docker build quietly uses BuildKit too, but buildx is where the advanced flags live, which is why the production chapters reach for docker buildx build as the default invocation.

For Driftwood, buildx is the command that carries the database password in as a build secret and keeps pip's wheel cache warm across builds. Both of those depend on the # syntax line and on BuildKit being the active builder — which, on a current install, it already is.

Common Mistakes
  • Copy-pasting old build advice that assumes strictly sequential execution and a position-based cache — BuildKit parallelizes and content-caches, so "instruction order is everything" reasoning is partly obsolete and the pre---secret workarounds it prescribes are unnecessary.
  • Omitting the # syntax=docker/dockerfile:1 line and then writing RUN --mount=type=secret … — without the frontend directive an older daemon rejects the mount syntax as a parse error before any step runs.
  • Assuming --build-arg is the only way to pass a build-time value because you have never enabled BuildKit — it leaves the value in docker history, and the secret mount that fixes it (topic 30) only exists under BuildKit.
  • Forcing DOCKER_BUILDKIT=0 to "get the old behavior" on a build that uses cache or secret mounts — those instructions do not exist in the legacy builder, so the build fails outright instead of falling back.
Best Practices
  • Put # syntax=docker/dockerfile:1 as the first line of every Dockerfile, so the build always uses a current frontend and the --mount instructions parse on any daemon.
  • Use docker buildx build (or confirm BuildKit is the active builder) for any image that needs secrets, cache mounts, or multi-platform output, since those are buildx and BuildKit features.
  • Let BuildKit's parallelism work for you by splitting genuinely independent setup into separate stages rather than chaining it into one long serial RUN.
  • Treat BuildKit as the baseline and drop the legacy-builder workarounds — they add noise and forfeit the content caching and secret handling you now get for free.
Comparable tools buildx the CLI plugin that drives BuildKit and exposes its full surface Kaniko builds OCI images in-cluster with no daemon, common in Kubernetes CI Buildpacks · ko produce images with no hand-written Dockerfile at all Podman · Buildah the daemonless build stack with its own engine

Knowledge Check

What can BuildKit do that the legacy line-by-line builder could not?

  • Build independent steps in parallel, cache by input content, and understand --mount secret and cache instructions
  • Produce a measurably smaller final image from the identical Dockerfile and instruction set
  • Run the resulting container faster at runtime by retuning the host process scheduler and CPU affinity for the workload
  • Encrypt the connection between the CLI and the daemon socket during the build

On a current Docker install, how do you get BuildKit?

  • It is the default — a plain docker build uses it, and older daemons opt in with DOCKER_BUILDKIT=1 or buildx
  • You install it from the package manager as a separate component alongside Docker Engine and enable its systemd service
  • You pass the --buildkit flag to every single docker build invocation and re-supply it on each rebuild
  • You export DOCKER_BUILDKIT=0 in the shell before building

Why does a Dockerfile that uses RUN --mount=type=secret need # syntax=docker/dockerfile:1 as its first line?

  • It fetches a current Dockerfile frontend so the --mount instructions parse even on an older daemon
  • It pins the base image to a specific @sha256: digest so every rebuild resolves the identical base bytes for reproducibility
  • It encrypts the secret value before it is mounted into the build step
  • It switches the build from the legacy line-by-line builder over to BuildKit

What is the relationship between docker build, docker buildx, and BuildKit?

  • BuildKit is the engine; plain docker build uses it quietly, and buildx is the plugin that exposes its full feature surface
  • buildx is a separate, competing build engine, shipped by a different vendor, that fully replaces and supersedes BuildKit altogether
  • Plain docker build always uses the legacy builder, and only buildx uses BuildKit
  • They are three interchangeable names for exactly the same command with identical features

You got correct