If you've run docker build in the last few years, you've used BuildKit — often without knowing it. It's the build engine that replaced Docker's legacy builder, and it's a genuine architectural leap: parallel, aggressively cacheable, secret-aware, and capable of running rootless. This guide explains what BuildKit is and why it matters.
The problem with the legacy builder
The original Docker builder processed a Dockerfile sequentially, top to bottom, one instruction per layer, with no understanding of which steps actually depended on each other. Consequences:
- No parallelism — independent steps still ran one after another.
- Caching was naive and line-order-sensitive; touching one early line busted everything after it.
- Secrets passed via build args leaked into image layers.
- Builds required a privileged daemon with root.
For a small image, fine. For a large monorepo or a multi-stage build, painfully slow and a little dangerous.
What BuildKit changes
BuildKit reimagines the build as a directed acyclic graph (DAG) of operations rather than a linear script. It analyzes the Dockerfile, figures out the real dependencies between steps, and then:
- Runs independent steps in parallel. Two unrelated stages build simultaneously.
- Skips unneeded work. In a multi-stage build, only the stages contributing to the final target are executed.
- Caches intelligently. It caches at the operation level and can import/export cache to external stores, so CI runs share cache.
- Handles secrets safely. Secrets are mounted for a single step and never persisted in a layer.
- Runs rootless. No privileged daemon required.
The DAG advantage, concretely
Consider a multi-stage build that compiles a frontend and a backend, then assembles them:
FROM node:20 AS frontend
WORKDIR /fe
COPY frontend/ .
RUN npm ci && npm run build
FROM golang:1.22 AS backend
WORKDIR /be
COPY backend/ .
RUN go build -o server .
FROM gcr.io/distroless/base
COPY --from=frontend /fe/dist /app/public
COPY --from=backend /be/server /app/server
CMD ["/app/server"]The legacy builder builds frontend fully, then backend, then assembles. BuildKit sees that frontend and backend don't depend on each other and builds them in parallel — often roughly halving wall-clock time on this shape of build.
Caching that actually helps
BuildKit's caching is where most real-world speedups come from. Two features stand out:
Layer ordering still matters
The oldest trick still applies — copy dependency manifests before source so dependency installs cache across source changes:
COPY package*.json ./
RUN npm ci # cached unless package*.json changes
COPY . . # source changes don't bust the installCache mounts
BuildKit adds persistent cache mounts that survive across builds without bloating the image:
# syntax=docker/dockerfile:1
RUN --mount=type=cache,target=/root/.npm \
npm ciThe /root/.npm cache persists between builds, so even a cache-busted npm ci reuses downloaded packages. The same pattern works for Go's module cache, pip's wheel cache, apt, and more.
Secrets without leaks
The legacy builder forced an ugly choice: pass a token as a build arg (leaks into history) or do contortions. BuildKit mounts secrets for a single RUN only:
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm cidocker build --secret id=npmrc,src=$HOME/.npmrc .The secret is available during that step and never written to any layer.
Rootless mode and why it matters for security
Traditionally, building images meant a daemon running as root — a meaningful attack surface, especially in shared CI. BuildKit can run rootless, building images without root privileges and without a Docker socket. For a multi-tenant build platform, this is essential: you don't want one tenant's build able to escalate via a privileged daemon.
How PandaStack uses BuildKit
PandaStack runs rootless BuildKit inside ephemeral Kubernetes Job pods. Each build gets a fresh, isolated pod; BuildKit builds your image without a host Docker socket and pushes it to Google Artifact Registry; the image is then deployed via Helm. This gives you BuildKit's speed and caching, plus strong isolation between builds — no shared privileged daemon, no leakage between tenants. (We go deeper on this in our ephemeral build pods architecture post.)
BuildKit vs the legacy builder
| Feature | Legacy builder | BuildKit |
|---|---|---|
| Execution | Sequential | Parallel (DAG) |
| Multi-stage pruning | No | Yes (only needed stages) |
| Cache mounts | No | Yes |
| External cache import/export | No | Yes |
| Build secrets | Leak-prone | First-class, ephemeral |
| Rootless | No | Yes |
Getting started locally
BuildKit is the default in modern Docker, but you can be explicit:
# Force BuildKit
DOCKER_BUILDKIT=1 docker build -t myapp .
# Or use buildx (BuildKit-powered)
docker buildx build -t myapp .Add # syntax=docker/dockerfile:1 at the top of your Dockerfile to unlock the newer features like cache and secret mounts.
References
- [BuildKit on GitHub](https://github.com/moby/buildkit)
- [Docker: BuildKit overview](https://docs.docker.com/build/buildkit/)
- [Dockerfile frontend syntax (mounts, secrets, cache)](https://docs.docker.com/reference/dockerfile/)
- [BuildKit rootless mode](https://github.com/moby/buildkit/blob/master/docs/rootless.md)
- [Google Artifact Registry](https://cloud.google.com/artifact-registry/docs)
---
You get BuildKit's speed and rootless security automatically on PandaStack — no Dockerfile syntax headers or buildx setup required. Push a repo and watch a parallel, cached build run at [dashboard.pandastack.io](https://dashboard.pandastack.io).