BuildKit is Docker's next-generation build engine, replacing the legacy builder with a dramatically faster, more secure, and more capable system. If you are still writing Dockerfiles without leveraging BuildKit features, you are leaving significant performance and security gains on the table. Builds that used to take minutes can finish in seconds with proper cache mounts. Secrets that used to leak into image layers can be handled safely. And multi-stage builds that used to execute sequentially can now run in parallel.

This guide covers every major BuildKit feature with practical examples you can start using today.

Enabling BuildKit

As of Docker 23.0, BuildKit is the default builder. For older versions, enable it explicitly:

# Option 1: Environment variable (per-command)
DOCKER_BUILDKIT=1 docker build -t myapp .

# Option 2: Docker daemon configuration (permanent)
# /etc/docker/daemon.json
{
  "features": {
    "buildkit": true
  }
}

# Option 3: Use docker buildx (recommended)
docker buildx build -t myapp .

# Verify BuildKit is active (look for "moby/buildkit" in output)
docker buildx version

Build Secrets

One of the most important BuildKit features is secure handling of build-time secrets. Before BuildKit, developers would COPY secrets into the image and then try to rm them—but they remained in previous layers. BuildKit secrets are never stored in the image:

# Dockerfile
# syntax=docker/dockerfile:1
FROM node:20-alpine

WORKDIR /app
COPY package*.json ./

# Mount the secret at build time - it's never stored in a layer
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci --production

COPY . .
CMD ["node", "server.js"]
# Build with the secret
docker buildx build \
  --secret id=npmrc,src=$HOME/.npmrc \
  -t myapp .

# Multiple secrets
docker buildx build \
  --secret id=npmrc,src=$HOME/.npmrc \
  --secret id=aws,src=$HOME/.aws/credentials \
  -t myapp .

The secret is mounted as a temporary file during the RUN instruction and is automatically removed afterward. It never appears in docker history or in any layer of the final image.

Warning: Never use ARG or ENV for secrets. Build arguments are visible in docker history and environment variables persist in the image. Always use --mount=type=secret.

SSH Forwarding

For cloning private Git repositories during the build, BuildKit can forward your SSH agent instead of copying private keys:

# Dockerfile
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine

RUN apk add --no-cache git openssh-client
RUN mkdir -p -m 0700 ~/.ssh && \
    ssh-keyscan github.com >> ~/.ssh/known_hosts

# Use SSH agent forwarding for private repos
RUN --mount=type=ssh \
    git clone [email protected]:myorg/private-lib.git /src/lib

WORKDIR /src/app
COPY . .
RUN go build -o /app ./cmd/server

FROM alpine:3.19
COPY --from=0 /app /usr/local/bin/app
CMD ["app"]
# Build with SSH forwarding
eval $(ssh-agent)
ssh-add ~/.ssh/id_ed25519
docker buildx build --ssh default -t myapp .

Cache Mounts

Cache mounts are arguably the single most impactful BuildKit feature for build performance. They persist a directory across builds, so package managers do not re-download everything on every build:

# Go application with module cache
FROM golang:1.22-alpine AS builder

WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
    go mod download

COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    CGO_ENABLED=0 go build -o /app ./cmd/server
# Node.js application with npm cache
FROM node:20-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci

COPY . .
RUN npm run build
# Python application with pip cache
FROM python:3.12-slim AS builder

WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --no-cache-dir -r requirements.txt

COPY . .
RUN python -m compileall .
# apt-get with cache mount (avoid re-downloading packages)
FROM ubuntu:22.04

RUN --mount=type=cache,target=/var/cache/apt \
    --mount=type=cache,target=/var/lib/apt/lists \
    apt-get update && apt-get install -y \
    curl \
    git \
    build-essential
Tip: Cache mounts can reduce build times by 50-90% for dependency-heavy projects. The cache persists across builds on the same machine, so the second build of a Go project with hundreds of dependencies can complete in seconds instead of minutes.

All Mount Types

BuildKit supports several mount types beyond secrets and caches:

Mount Type Purpose Example
cache Persistent cache directory across builds --mount=type=cache,target=/root/.cache
secret Temporary secret file, never stored in layers --mount=type=secret,id=key,target=/run/key
ssh SSH agent forwarding --mount=type=ssh
bind Read-only bind mount from build context or stage --mount=type=bind,from=builder,source=/app,target=/src
tmpfs Temporary in-memory filesystem --mount=type=tmpfs,target=/tmp

Parallel Multi-Stage Builds

BuildKit analyzes the dependency graph of your Dockerfile stages and executes independent stages in parallel. This is a free performance win if you structure your Dockerfile correctly:

# syntax=docker/dockerfile:1
# These three stages run IN PARALLEL because they are independent

FROM node:20-alpine AS frontend
WORKDIR /app
COPY frontend/package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
COPY frontend/ .
RUN npm run build

FROM golang:1.22-alpine AS backend
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
    CGO_ENABLED=0 go build -o /server ./cmd/server

FROM golang:1.22-alpine AS migrations
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
RUN --mount=type=cache,target=/root/.cache/go-build \
    CGO_ENABLED=0 go build -o /migrate ./cmd/migrate

# Final stage depends on all three - they run in parallel above
FROM alpine:3.19
RUN apk add --no-cache ca-certificates
COPY --from=frontend /app/dist /static
COPY --from=backend /server /usr/local/bin/server
COPY --from=migrations /migrate /usr/local/bin/migrate
EXPOSE 8080
CMD ["server"]

BuildKit detects that frontend, backend, and migrations stages have no dependency on each other and builds them simultaneously. The final stage only starts after all three complete.

Heredocs in Dockerfiles

BuildKit enables heredoc syntax, making multi-line scripts and configuration files much cleaner:

# syntax=docker/dockerfile:1
FROM python:3.12-slim

# Multi-line script without backslash line continuations
RUN <<EOF
apt-get update
apt-get install -y --no-install-recommends curl git
rm -rf /var/lib/apt/lists/*
EOF

# Inline file creation
COPY <<EOF /etc/nginx/conf.d/default.conf
server {
    listen 80;
    server_name _;
    location / {
        proxy_pass http://app:8080;
        proxy_set_header Host \$host;
        proxy_set_header X-Real-IP \$remote_addr;
    }
}
EOF

# Multiple heredocs in one RUN
RUN <<INSTALL && <<CONFIGURE
apt-get update
apt-get install -y nginx
INSTALL
nginx -t
systemctl enable nginx
CONFIGURE

Build Arguments and Conditional Logic

Combine build arguments with BuildKit for flexible, parameterized builds:

# syntax=docker/dockerfile:1
ARG GO_VERSION=1.22
ARG ALPINE_VERSION=3.19

FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS builder

ARG TARGETOS
ARG TARGETARCH
ARG VERSION=dev
ARG COMMIT=unknown

WORKDIR /src
COPY . .

RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \
    -ldflags "-s -w -X main.version=${VERSION} -X main.commit=${COMMIT}" \
    -o /app ./cmd/server

FROM alpine:${ALPINE_VERSION}
COPY --from=builder /app /usr/local/bin/app
CMD ["app"]
# Build with custom arguments
docker buildx build \
  --build-arg GO_VERSION=1.22 \
  --build-arg VERSION=$(git describe --tags) \
  --build-arg COMMIT=$(git rev-parse --short HEAD) \
  -t myapp:$(git describe --tags) .

Build Cache Import/Export

One of BuildKit's killer features for CI/CD is the ability to export and import build caches. This makes CI builds as fast as local builds:

# Export cache to a registry (inline mode - stored in the image)
docker buildx build \
  --cache-to type=inline \
  -t registry.example.com/myapp:latest \
  --push .

# Import cache from the registry
docker buildx build \
  --cache-from type=registry,ref=registry.example.com/myapp:latest \
  -t registry.example.com/myapp:new-version \
  --push .

# Export/import with separate cache image (more flexible)
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 \
  -t registry.example.com/myapp:latest \
  --push .

# Local directory cache (useful for CI systems with persistent storage)
docker buildx build \
  --cache-to type=local,dest=/tmp/buildcache \
  --cache-from type=local,src=/tmp/buildcache \
  -t myapp .

The mode=max option exports all layers (including intermediate stages), not just the final image layers. This maximizes cache hits for subsequent builds.

GitHub Actions CI Example

# .github/workflows/build.yml
name: Build and Push
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            VERSION=${{ github.ref_name }}
            COMMIT=${{ github.sha }}

BuildKit Output Types

BuildKit can produce various output formats beyond just Docker images:

# Output as local files (extract build artifacts)
docker buildx build --output type=local,dest=./output .

# Output as a tar archive
docker buildx build --output type=tar,dest=./image.tar .

# Output as an OCI image layout
docker buildx build --output type=oci,dest=./oci-image.tar .

# Load into Docker daemon (default for single-platform)
docker buildx build --load -t myapp .

# Push directly to registry
docker buildx build --push -t registry.example.com/myapp:latest .
Tip: The --output type=local option is incredibly useful for extracting compiled binaries from a build without creating an image. Combined with multi-stage builds, you can use Docker as a cross-compilation environment.

BuildKit Front-end Features

The # syntax= directive at the top of your Dockerfile selects the BuildKit frontend. Different frontends unlock different features:

# Use the latest stable Dockerfile frontend
# syntax=docker/dockerfile:1

# Use a specific version
# syntax=docker/dockerfile:1.7

# Use the labs channel for experimental features
# syntax=docker/dockerfile:1-labs

The labs channel includes experimental features that may change. Currently, this includes:

  • Here-documents (now stable in 1.4+)
  • COPY --parents for preserving directory structure
  • COPY --chmod for setting permissions during copy
# syntax=docker/dockerfile:1
FROM alpine:3.19

# Copy with permissions (no separate chmod RUN needed)
COPY --chmod=755 scripts/ /usr/local/bin/

# Copy preserving parent directory structure
COPY --parents src/config/ /app/

Debugging BuildKit Builds

When builds fail, BuildKit provides better debugging tools than the legacy builder:

# Show build progress in plain text (see each step)
docker buildx build --progress=plain -t myapp .

# Export the build graph as JSON
docker buildx build --metadata-file metadata.json -t myapp .

# Interactive debugging with buildx debug (experimental)
docker buildx debug build -t myapp .

# Show detailed build timing
docker buildx build --progress=plain -t myapp . 2>&1 | grep "DONE"

For CI/CD pipelines, usulnet can track image builds and their associated metadata, helping teams understand build performance trends and cache hit rates across their Docker infrastructure.

Performance Comparison

Feature Legacy Builder BuildKit
Parallel stage execution No (sequential) Yes (automatic)
Cache mounts No Yes
Build secrets No (leak into layers) Yes (never stored)
Remote cache No Yes (registry, S3, GHA)
Skipping unused stages No (builds everything) Yes (only needed stages)
Multi-platform builds No Yes (via buildx)
Typical speedup Baseline 2x-10x faster

Conclusion

BuildKit transforms Docker builds from a slow, sequential, and insecure process into a fast, parallel, and security-conscious workflow. The three features with the highest impact are cache mounts (for dependency caching), build secrets (for secure credential handling), and parallel multi-stage builds (for faster compilation). Add remote cache export/import for CI/CD, and your Docker builds will be both faster and more reliable across every environment.

Start by adding # syntax=docker/dockerfile:1 to the top of your Dockerfiles and converting your RUN instructions to use cache mounts. The improvement is immediate and requires minimal refactoring.