What Is BuildKit?
BuildKit is a toolkit for converting source code to build artifacts in an efficient, expressive, and repeatable manner. Originally developed at Moby (Docker's open-source project), it replaced Docker's legacy builder as the default engine in Docker 23.0 (February 2023).
Legacy Builder vs BuildKit
| Feature | Legacy Builder | BuildKit |
|---|---|---|
| Execution model | Sequential (line by line) | Parallel DAG execution |
| Cache | Layer-based only | Layer + mount caches + registry cache |
| Secret handling | Build args (visible in history) | tmpfs mounts (never in layers) |
| SSH access | Copy keys into image (insecure) | SSH agent forwarding |
| Multi-platform | Not supported | Native cross-compilation |
| Output formats | Docker image only | Image, tar, local directory, OCI |
| Garbage collection | Manual prune | Automatic with configurable policies |
| Progress output | Step-by-step text | Rich terminal UI with timing |
Enabling BuildKit
If you're running Docker 23.0+, BuildKit is already your default builder. For older installations or explicit control, here are the activation methods:
Method 1: Environment Variable
# Enable for a single build
DOCKER_BUILDKIT=1 docker build -t myapp .
# Enable permanently in your shell profile (~/.bashrc or ~/.zshrc)
export DOCKER_BUILDKIT=1
Method 2: Docker Daemon Configuration
{
"features": {
"buildkit": true
},
"builder": {
"gc": {
"enabled": true,
"defaultKeepStorage": "20GB"
}
}
}
# Apply daemon config (Linux)
sudo systemctl restart docker
# Verify BuildKit is active
docker build --help | grep buildkit
docker info | grep -i buildkit
# BuildKit: true
Method 3: Docker Buildx (Recommended)
# Buildx is the CLI plugin that wraps BuildKit
# Check if available
docker buildx version
# github.com/docker/buildx v0.14.0
# Create a new builder instance with more features
docker buildx create --name mybuilder --use --bootstrap
# Inspect the builder
docker buildx inspect mybuilder
# Name: mybuilder
# Driver: docker-container
# Nodes:
# Name: mybuilder0
# Endpoint: unix:///var/run/docker.sock
# Status: running
# Platforms: linux/amd64, linux/arm64, linux/arm/v7
# Build with buildx (equivalent to docker build but more options)
docker buildx build -t myapp:latest --load .
docker build already uses BuildKit. Use docker buildx build when you need advanced features like multi-platform builds, custom builders, or remote build instances. Both produce identical images for standard builds.
Parallel Stage Execution
BuildKit analyzes the dependency graph of your multi-stage Dockerfile and executes independent stages simultaneously. This can cut build times dramatically for complex Dockerfiles with multiple independent stages.
flowchart LR
subgraph Legacy["Legacy Builder (Sequential)"]
direction TB
A1[Stage: deps
45s] --> B1[Stage: test
30s]
B1 --> C1[Stage: build
20s]
C1 --> D1[Stage: runtime
5s]
end
subgraph BK["BuildKit (Parallel)"]
direction TB
A2[Stage: deps
45s] --> C2[Stage: build
20s]
A2 --> B2[Stage: test
30s]
C2 --> D2[Stage: runtime
5s]
end
style Legacy fill:#fff3f3,stroke:#BF092F
style BK fill:#f0fff7,stroke:#3B9797
In this example, the test and build stages both depend on deps but not on each other. The legacy builder runs them sequentially (100s total). BuildKit runs them in parallel (70s total — a 30% improvement).
# syntax=docker/dockerfile:1
# Multi-stage Dockerfile with parallel-friendly stages
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# These two stages run IN PARALLEL (both depend only on 'deps')
FROM deps AS test
COPY . .
RUN npm run lint
RUN npm run test
FROM deps AS build
COPY . .
RUN npm run build
# Final stage — depends on 'build' (test must also pass)
FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json .
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]
# Build with progress=plain to see parallel execution
docker build --progress=plain -t myapp .
# You'll see output like:
# #5 [test 1/3] COPY . .
# #6 [build 1/2] COPY . . ← running simultaneously!
# #5 [test 2/3] RUN npm run lint
# #6 [build 2/2] RUN npm run build
Advanced Cache Mounts
Cache mounts are BuildKit's killer feature for build performance. They persist a directory across builds without including it in the image layers — perfect for package manager caches that would otherwise be re-downloaded on every build.
RUN --mount=type=cache,target=/path creates a persistent cache directory. It survives between builds but is never included in the final image. Think of it as a build-time volume.
APT Package Cache
# syntax=docker/dockerfile:1
# ❌ Without cache mount: downloads packages every build (~30s)
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# ✅ With cache mount: downloads only new/updated packages (~3s)
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt \
apt-get update && apt-get install -y --no-install-recommends curl
Python pip Cache
# syntax=docker/dockerfile:1
# ❌ Without cache: downloads all wheels every build
RUN pip install --no-cache-dir -r requirements.txt
# ✅ With cache mount: reuses previously downloaded wheels
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
Node.js npm/yarn Cache
# syntax=docker/dockerfile:1
# npm with cache mount
RUN --mount=type=cache,target=/root/.npm \
npm ci
# yarn with cache mount
RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
yarn install --frozen-lockfile
# pnpm with cache mount
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile
Go Module & Build Cache
# syntax=docker/dockerfile:1
# Go benefits from TWO cache mounts: module downloads + compilation
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o /app/server .
Cache Mount Performance Impact
| Package Manager | First Build | Rebuild (no changes) | Rebuild (1 new dep) |
|---|---|---|---|
| apt (no cache) | 32s | 32s | 32s |
| apt (with cache mount) | 32s | 4s | 8s |
| pip (no cache) | 45s | 45s | 45s |
| pip (with cache mount) | 45s | 3s | 6s |
| npm ci (no cache) | 28s | 28s | 28s |
| npm ci (with cache mount) | 28s | 5s | 9s |
| go build (no cache) | 60s | 60s | 60s |
| go build (with cache mount) | 60s | 2s | 8s |
Secret Mounts
Secret mounts solve the fundamental problem of needing credentials during build without embedding them in image layers. Unlike build arguments (which appear in docker history), secrets are mounted as tmpfs and never persist in any layer.
ARG or ENV for sensitive values during builds. Even in multi-stage builds, intermediate layers containing secrets may be cached or visible via docker history --no-trunc. Always use --mount=type=secret.
# syntax=docker/dockerfile:1
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
# Mount secret at build time — accessible only during this RUN
RUN --mount=type=secret,id=pip_conf,target=/etc/pip.conf \
pip install -r requirements.txt
# Alternative: read secret as environment variable
RUN --mount=type=secret,id=api_key \
API_KEY=$(cat /run/secrets/api_key) && \
curl -H "Authorization: Bearer $API_KEY" https://registry.example.com/config > config.json
Build commands pass secrets from the host:
# Pass a file as a secret
docker build --secret id=pip_conf,src=$HOME/.pip/pip.conf -t myapp .
# Pass a secret from an environment variable
docker build --secret id=api_key,env=MY_API_KEY -t myapp .
# Multiple secrets
docker build \
--secret id=npm_token,src=.npmrc \
--secret id=github_token,env=GITHUB_TOKEN \
-t myapp .
# Verify: secret is NOT in image history
docker history myapp --no-trunc | grep -i secret
# (no output — secrets are invisible)
Private npm Registry Example
# syntax=docker/dockerfile:1
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
# Mount .npmrc with private registry token
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
SSH Forwarding
SSH mounts forward your local SSH agent into the build container, allowing operations like cloning private Git repositories without copying SSH keys into the image. The SSH socket is available only during the RUN instruction and leaves no trace in layers.
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine AS build
# Install git and SSH client
RUN apk add --no-cache git openssh-client
# Configure SSH to trust GitHub's host key
RUN mkdir -p -m 0700 ~/.ssh && \
ssh-keyscan github.com >> ~/.ssh/known_hosts
WORKDIR /app
COPY go.mod go.sum ./
# Use SSH agent forwarding to download private modules
RUN --mount=type=ssh \
git config --global url."git@github.com:".insteadOf "https://github.com/" && \
go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /server .
FROM scratch
COPY --from=build /server /server
ENTRYPOINT ["/server"]
# Ensure SSH agent is running with your key loaded
eval $(ssh-agent)
ssh-add ~/.ssh/id_ed25519
# Build with SSH forwarding
docker build --ssh default -t myapp .
# Or specify a specific key file
docker build --ssh default=$HOME/.ssh/id_ed25519 -t myapp .
SSH Agent Socket Forwarding
When you specify --mount=type=ssh, BuildKit creates a Unix socket inside the build container connected to your local SSH agent via gRPC. The build process can authenticate with remote servers through this socket without the private key ever touching the container filesystem. After the RUN instruction completes, the socket is removed — it exists only in memory during execution.
Build Arguments & Named Build Contexts
BuildKit extends build arguments with named contexts — the ability to pull files from multiple directories, images, or Git repositories during a single build.
Named Build Contexts
# syntax=docker/dockerfile:1
# Reference a named context (provided at build time)
FROM node:20-alpine AS build
WORKDIR /app
COPY . .
RUN npm run build
FROM nginx:alpine
# Copy from the build stage
COPY --from=build /app/dist /usr/share/nginx/html
# Copy from a named context "configs"
COPY --from=configs nginx.conf /etc/nginx/nginx.conf
# Provide named contexts at build time
docker buildx build \
--build-context configs=./deploy/nginx \
--build-context shared=../shared-libraries \
-t myapp .
# Use a remote image as a context
docker buildx build \
--build-context base=docker-image://ubuntu:24.04 \
-t myapp .
# Use a Git repository as a context
docker buildx build \
--build-context config=https://github.com/org/config.git#main \
-t myapp .
Dynamic Base Images with ARG
# syntax=docker/dockerfile:1
ARG BASE_IMAGE=node:20-alpine
ARG NODE_VERSION=20
FROM ${BASE_IMAGE} AS runtime
# ARG must be re-declared after FROM to use in this stage
ARG NODE_VERSION
RUN echo "Running on Node ${NODE_VERSION}"
# Override at build time for different environments
docker build --build-arg BASE_IMAGE=node:20-slim -t myapp:debian .
docker build --build-arg BASE_IMAGE=node:20-alpine -t myapp:alpine .
Inline Cache Export for CI/CD
In CI/CD pipelines, every build starts fresh — no local cache. BuildKit solves this by exporting cache metadata to a registry, allowing subsequent builds to pull cache layers remotely instead of rebuilding from scratch.
# Export cache inline (stored in the image manifest)
docker buildx build \
--cache-to type=inline \
--push \
-t registry.example.com/myapp:latest .
# Next build imports cache from the registry
docker buildx build \
--cache-from type=registry,ref=registry.example.com/myapp:latest \
-t registry.example.com/myapp:latest .
# Separate cache image (doesn't bloat your app image)
docker buildx build \
--cache-to type=registry,ref=registry.example.com/myapp:cache,mode=max \
--cache-from type=registry,ref=registry.example.com/myapp:cache \
--push \
-t registry.example.com/myapp:latest .
GitHub Actions with BuildKit Cache
# .github/workflows/build.yml
name: Build and Push
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
mode=min (default) caches only the final stage's layers. mode=max caches ALL intermediate stages — better for multi-stage builds where early stages change rarely. Use mode=max in CI/CD for maximum cache hits.
Reproducible Builds
A reproducible build produces a bit-for-bit identical image regardless of when or where it's built. This matters for supply chain security — you can verify that a deployed image was built from specific source code without trusting the build system.
Why Reproducibility Matters
- Supply chain verification — Prove an image comes from specific source
- Audit trails — Identical hash = identical content
- Cache efficiency — Deterministic builds maximize cache hits
- Regulatory compliance — Some industries require build provenance
Achieving Reproducibility
# syntax=docker/dockerfile:1
# Pin base image by digest (immutable reference)
FROM golang:1.22.0@sha256:a84e0fbb0e0e... AS build
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Reproducible build flags
RUN CGO_ENABLED=0 \
GOFLAGS="-trimpath" \
go build -ldflags="-s -w -buildid=" -o /server .
FROM scratch
COPY --from=build /server /server
ENTRYPOINT ["/server"]
# Set SOURCE_DATE_EPOCH for reproducible timestamps
SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) \
docker buildx build \
--build-arg SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH \
--output type=oci,dest=image.tar,rewrite-timestamp=true \
-t myapp:latest .
# Verify reproducibility: build twice and compare digests
docker buildx build --load -t myapp:test1 .
docker buildx build --load -t myapp:test2 .
docker inspect myapp:test1 --format='{{.Id}}'
docker inspect myapp:test2 --format='{{.Id}}'
# Should produce identical IDs
Buildx & Multi-Platform Builds
Multi-platform builds produce images for multiple CPU architectures (amd64, arm64, armv7) from a single Dockerfile and build command. This is essential for supporting both x86 servers and ARM-based devices (Raspberry Pi, Apple Silicon, AWS Graviton).
flowchart TD
A["docker buildx build
--platform linux/amd64,linux/arm64"] --> B["BuildKit Engine"]
B --> C["Build for amd64"]
B --> D["Build for arm64"]
C --> E["Image manifest
(amd64)"]
D --> F["Image manifest
(arm64)"]
E --> G["Multi-arch
Manifest List"]
F --> G
G --> H["Registry Push"]
style B fill:#f0f7ff,stroke:#16476A
style G fill:#f0fff7,stroke:#3B9797
# Create a builder that supports multi-platform
docker buildx create --name multiarch --driver docker-container --use --bootstrap
# Build for multiple platforms simultaneously
docker buildx build \
--platform linux/amd64,linux/arm64,linux/arm/v7 \
--push \
-t registry.example.com/myapp:latest .
# Build for a specific platform (useful for testing)
docker buildx build \
--platform linux/arm64 \
--load \
-t myapp:arm64 .
# Inspect the multi-arch manifest
docker buildx imagetools inspect registry.example.com/myapp:latest
# Name: registry.example.com/myapp:latest
# MediaType: application/vnd.oci.image.index.v1+json
# Manifests:
# Name: ...@sha256:abc123
# Platform: linux/amd64
# Name: ...@sha256:def456
# Platform: linux/arm64
# Name: ...@sha256:ghi789
# Platform: linux/arm/v7
Platform-Aware Dockerfiles
# syntax=docker/dockerfile:1
# BuildKit provides TARGETPLATFORM, TARGETOS, TARGETARCH automatically
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Cross-compile for the target platform
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \
go build -ldflags="-s -w" -o /server .
FROM scratch
COPY --from=build /server /server
ENTRYPOINT ["/server"]
BUILDPLATFORM vs TARGETPLATFORM
BUILDPLATFORM is the architecture of the machine running the build (your CI server, typically amd64). TARGETPLATFORM is what you're building FOR. By using FROM --platform=$BUILDPLATFORM for build stages, you run the compiler natively (fast) and only cross-compile the final binary. Without this, QEMU emulation would run the entire build process under emulation (10-50× slower).
Build Performance Tips
| Tip | Impact | Implementation |
|---|---|---|
| Use .dockerignore | Reduces context transfer by 90%+ | Exclude node_modules, .git, build artifacts |
| Order layers by change frequency | Maximizes cache hits | Dependencies first, source code last |
| Use cache mounts | Eliminates re-downloads | --mount=type=cache for pkg managers |
| Design parallel stages | 30-50% faster builds | Independent test/build stages |
| Use registry cache in CI | Restores cache on fresh runners | --cache-from/--cache-to type=gha |
| Pin dependencies | Deterministic builds, stable cache | Lock files (package-lock.json, go.sum) |
| Cross-compile with BUILDPLATFORM | Native speed for multi-arch | FROM --platform=$BUILDPLATFORM |
| Minimize build context | Faster context hashing | Keep build directory lean |
Diagnosing Slow Builds
# Show detailed timing for each step
docker build --progress=plain -t myapp . 2>&1 | tee build.log
# Find the slowest steps
grep "DONE\|CACHED" build.log | sort -t'=' -k2 -n
# Analyze build history layers
docker history myapp --format "{{.Size}}\t{{.CreatedBy}}" | sort -rh | head -10
# Check what's in the build context (should match .dockerignore)
docker build --no-cache --progress=plain . 2>&1 | grep "transferring context"
# => transferring context: 2.1MB (good) vs 800MB (bad — missing .dockerignore)
Exercises
Cache Mount Benchmark
Take a Python project with 20+ dependencies. Build it twice without cache mounts, noting the time. Then add --mount=type=cache,target=/root/.cache/pip and rebuild twice. Measure the difference on the second build. You should see a 5-10× speedup.
Secret Mount for Private Registry
Set up a Dockerfile that installs packages from a private npm registry using --mount=type=secret. Verify the token doesn't appear in docker history --no-trunc. Compare this with the insecure approach of using ARG NPM_TOKEN.
Multi-Platform Build
Create a simple Go or Rust application. Build it for linux/amd64 and linux/arm64 simultaneously using docker buildx build --platform. Push to a registry and use docker buildx imagetools inspect to verify both manifests exist.
CI/CD Cache Pipeline
Set up a GitHub Actions workflow that uses BuildKit's GitHub Actions cache (type=gha). Push a change that only modifies source code (not dependencies) and verify that the dependency installation step hits the cache on the second run.
Conclusion & Next Steps
BuildKit transforms Docker builds from a slow, sequential process into a fast, secure, and intelligent build system. The key capabilities to adopt immediately are:
- Cache mounts — Eliminate re-downloads of packages with
--mount=type=cachefor 5-30× faster rebuilds - Secret mounts — Handle credentials without ever exposing them in image layers
- Parallel stages — Structure Dockerfiles so independent stages run simultaneously
- Registry cache — Export/import cache in CI/CD for fast builds on ephemeral runners
- Multi-platform — Build for amd64 and arm64 from a single command
- Reproducibility — Pin digests and set SOURCE_DATE_EPOCH for verifiable builds
Combined with the multi-stage patterns from Part 8, BuildKit gives you production-grade builds that are fast, secure, and reproducible — the three pillars of modern CI/CD infrastructure.
Next in the Series
In Part 10: Docker Networking Fundamentals, we explore how containers communicate — bridge networks for isolation, host networking for performance, overlay networks for multi-host clusters, and DNS-based service discovery that makes container-to-container communication seamless.