Docker's BuildKit builder replaced the classic image builder as the default. What does BuildKit do differently, and what does that change about how you write a Dockerfile?
answer
- legacy = linear steps; BuildKit = DAG solver
- parallel stages, unused stages skipped
- context streamed lazily, not tarred up front
- # syntax=docker/dockerfile:1 unlocks mounts and heredocs
- buildx needs --load or --push to get the image out
basics
~20 sThe classic builder ran instructions strictly top to bottom, one container per step. BuildKit builds a dependency graph, so independent stages run in parallel, unused stages are skipped, and only the build-context files actually needed are transferred. It also adds features the old builder lacks: secret, cache and ssh mounts, and multi-platform builds.
solid answer
~50 sThe legacy builder executed a Dockerfile as a linear sequence, materialising an intermediate image per instruction and sending the whole build context up front. BuildKit converts the Dockerfile into a **DAG of low-level build operations** and solves it. Practical consequences: - **Parallelism**: independent multi-stage branches build concurrently. - **Pruning**: stages that the requested target does not depend on are never built. - **Lazy context transfer**: the client streams only the files actually referenced, and honours `.dockerignore` more efficiently. - **Better cache**: content-addressed cache keys, plus importable and exportable caches for CI. - **New syntax**: `RUN --mount=type=secret|cache|bind|ssh`, `COPY --link`, heredocs, all gated behind a `# syntax=docker/dockerfile:1` line that pulls a versioned frontend image. So I write Dockerfiles differently: multiple parallel build stages are now cheap, I use cache mounts for package managers instead of contorting layer order, and I never bake credentials into layers because secret mounts exist.
code
dockerfile · 14 lines# syntax=docker/dockerfile:1
FROM node:22 AS web
WORKDIR /w
COPY web/ .
RUN --mount=type=cache,target=/root/.npm npm ci && npm run build
FROM golang:1.23 AS api
WORKDIR /a
COPY api/ .
RUN --mount=type=cache,target=/root/.cache/go-build go build -o /out/api ./...
FROM gcr.io/distroless/base-debian12
COPY --from=api /out/api /api
COPY --from=web /w/dist /wwwgo deeper
Know BuildKit is the default builder, that it parallelises independent stages and skips unused ones, and that new mount syntax exists.
Explain the DAG model, lazy context transfer and content-addressed cache, and use the syntax directive plus cache mounts deliberately.
Connect it to CI throughput: exported registry caches, --target for test stages, secret mounts replacing build-arg credentials, and builder driver choices.
Decide the org build platform: shared remote builders versus per-runner builders, cache backend and retention, frontend version pinning, and how build provenance and SBOMs plug in.
## Two builders Until Docker Engine 23, docker build used the legacy builder inside the daemon. It walked the Dockerfile top to bottom, ran each instruction in a temporary container, committed the result as a layer, and moved on. Since Engine 23 BuildKit is the default, and it is the only builder that receives new features. BuildKit works differently. A frontend parses the Dockerfile into LLB (low-level build definition), a directed acyclic graph of operations such as "fetch this image", "exec this command in this filesystem", "copy these paths". A solver then evaluates that graph. ## What the graph buys you **Parallel execution.** In a multi-stage Dockerfile where two stages both derive from a base and neither depends on the other, the legacy builder ran them sequentially because the file was a list. BuildKit sees they are independent and runs them at once, which on a multi-core runner can nearly halve wall-clock time for builds that compile a frontend and a backend. **Dead stage elimination.** With docker build --target=test, BuildKit builds only what the test stage transitively needs. The legacy builder built every preceding stage in file order regardless. **Lazy, incremental context.** The legacy builder tarred the entire build context and sent it to the daemon before doing anything, which is why a stray node_modules or .git could make builds crawl. BuildKit's client session streams only the files the build actually references, and re-sends only changes between builds. **Content-addressed caching.** Cache keys are computed from the content of inputs rather than only instruction strings and parent-layer identity, and results are cacheable and exportable. That enables --cache-from and --cache-to against a registry, which is what makes ephemeral CI runners viable. **Extended Dockerfile syntax.** Enabling a versioned frontend with the first line # syntax=docker/dockerfile:1 lets you use features that ship independently of your Docker Engine version: RUN --mount=type=cache for package manager caches, --mount=type=secret for credentials that never touch a layer, --mount=type=ssh to forward an agent for private git dependencies, --mount=type=bind to read from another stage without copying, COPY --link for relocatable layers, and heredoc syntax for multi-line scripts. **Multi-platform builds.** Through docker buildx, one invocation can produce images for several architectures and push them as a single manifest list. ## How this changes your Dockerfile style On the legacy builder, the dominant tactic was manual layer choreography: order instructions so that rarely changing steps sit above frequently changing ones, and chain everything into one giant RUN to avoid extra layers and to delete caches within the same layer. That advice is still partly valid, but BuildKit removes much of the pain. Package manager caches belong in a cache mount rather than being deleted at the end of a RUN. Credentials belong in a secret mount rather than in a build arg someone tries to scrub. Extra stages are close to free, so splitting work into parallel stages is a real optimisation rather than a cost. ## Practical notes BuildKit is enabled by default in modern Docker Desktop and Engine; on older installs it is DOCKER_BUILDKIT=1. docker buildx is the CLI that exposes the fuller feature set and manages builder instances, including containerised builders and remote builders. Output is not automatically loaded into the local image store when using a non-default driver, which is why builds sometimes appear to succeed and then docker images shows nothing: you need --load for the local store or --push for a registry. One behavioural difference to expect in interviews: BuildKit's default log output is condensed and stages may interleave, so "the output looks different and the steps are not in file order" is not a bug, it is the graph being solved concurrently.
- What does the '# syntax=docker/dockerfile:1' line at the top of a Dockerfile actually do?It tells BuildKit to fetch that frontend image from the registry and use it to parse the Dockerfile, so the available instruction syntax is decoupled from the installed Docker Engine version. The :1 tag tracks the latest 1.x frontend, so you get new features and fixes without upgrading the daemon. Without it, you are limited to the syntax the built-in frontend supports, which is why mount options can fail with a parse error on an otherwise modern engine.
- Your buildx build reports success but 'docker images' does not show the image. Why?With a non-default buildx driver (such as docker-container), the build result stays in BuildKit's own cache and is not exported to the local Docker image store unless you ask for it. Add --load to import it locally, or --push to send it straight to a registry. Multi-platform builds cannot be --load-ed into the classic store at all, so they are normally pushed instead.
The legacy builder is a cook following a recipe line by line; BuildKit reads the whole recipe first, sees that the sauce and the dough never touch, and starts both burners at once while skipping the dessert nobody ordered.
saying these in an interview costs you the question
- Believing you must set DOCKER_BUILDKIT=1 on current Docker versions, unaware it is the default
- Thinking BuildKit only makes builds faster and adds no new Dockerfile syntax
- Expecting instructions to execute strictly in file order and calling interleaved output a bug
- Assuming the whole build context is always uploaded before the build starts
- Forgetting --load or --push and concluding the build silently failed