Skip to content
Codeloom
Docker

Docker BuildKit Advanced Features You Should Be Using

Master BuildKit's cache mounts, secret mounts, SSH forwarding, and parallel builds to speed up Docker image creation and keep secrets out of layers.

·6 min read · By Codeloom
Intermediate 10 min read

What you'll learn

  • How BuildKit differs from the legacy Docker builder
  • Cache mounts for package managers and compilers
  • Secret mounts that never leak into image layers
  • SSH forwarding for private Git repos during builds
  • Parallel stage execution in multi-stage builds

Prerequisites

None — this post is self-contained.

BuildKit replaced Docker’s legacy builder as the default in Docker Engine 23.0, yet most teams still write Dockerfiles that never touch its advanced features. That means slower builds, leaked secrets in layers, and missed parallelism. This guide walks through the features that matter most in production.

Enabling BuildKit

On Docker Desktop and Engine 23.0 and later, BuildKit is already the default. On older engines, export the environment variable before building:

export DOCKER_BUILDKIT=1
docker build -t myapp .

You can also use the docker buildx build command, which always routes through BuildKit and unlocks additional flags.

Cache Mounts for Package Managers

Every time you run apt-get install or pip install, the builder downloads packages from scratch unless you carefully manage layer caching. BuildKit’s --mount=type=cache keeps a persistent cache directory across builds without baking it into the final image.

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

WORKDIR /app
COPY requirements.txt .

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

COPY . .
CMD ["python", "main.py"]

The /root/.cache/pip directory persists between builds on the same machine. The --no-cache-dir flag tells pip not to create its own redundant cache, while BuildKit manages the shared one.

This works for any package manager. For Go:

RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go build -o /app/server ./cmd/server

For Node.js with npm:

RUN --mount=type=cache,target=/root/.npm \
    npm ci --prefer-offline

Cache mounts can cut repeated build times from minutes to seconds, especially in CI where layer caches are often cold but volume caches can be persisted.

Secret Mounts

Before BuildKit, developers passed secrets as build arguments. The problem is that ARG values are stored in image metadata and intermediate layers, making them trivially extractable with docker history.

Secret mounts solve this by making a file available during a single RUN instruction without ever writing it to a layer:

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

WORKDIR /app
COPY package*.json ./

RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm ci

COPY . .
CMD ["node", "server.js"]

Build with:

docker build --secret id=npmrc,src=$HOME/.npmrc -t myapp .

The .npmrc file is available only during that RUN step. It never appears in docker history or any layer tar. You can mount multiple secrets in one command:

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

SSH Forwarding

Cloning private Git repositories during a build used to require copying SSH keys into the image, a serious security risk even with multi-stage builds. BuildKit’s SSH mount forwards your host SSH agent:

# syntax=docker/dockerfile:1
FROM golang:1.22 AS builder

RUN --mount=type=ssh \
    git clone git@github.com:yourorg/private-lib.git /src/lib

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

Build with:

docker build --ssh default -t myapp .

The --ssh default flag forwards your current SSH agent. No keys are copied, and nothing persists in any layer.

Parallel Stage Execution

BuildKit analyzes your multi-stage Dockerfile and runs independent stages concurrently. The legacy builder executes every stage sequentially, even when there is no dependency between them.

# syntax=docker/dockerfile:1

# These two stages run in parallel
FROM node:20-alpine AS frontend
WORKDIR /app
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build

FROM golang:1.22-alpine AS backend
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . .
RUN go build -o /server ./cmd/server

# Final stage waits for both
FROM alpine:3.20
COPY --from=frontend /app/dist /srv/static
COPY --from=backend /server /usr/local/bin/server
CMD ["server"]

BuildKit detects that frontend and backend share no dependencies, so it builds them simultaneously. On a CI runner with multiple cores, this can halve total build time.

Build Outputs and Exporters

BuildKit can export results to places other than the local Docker image store. This is useful for CI pipelines that need tarballs or that push directly to a registry without loading locally.

# Export to a tar file
docker buildx build --output type=tar,dest=image.tar -t myapp .

# Push directly to a registry (no local load)
docker buildx build --push -t registry.example.com/myapp:latest .

# Export just the filesystem (no Docker metadata)
docker buildx build --output type=local,dest=./output .

The local exporter is particularly useful for extracting compiled binaries from builder stages without creating a final image at all.

Inline Build Cache for CI

In CI, you rarely have local layer cache from previous runs. BuildKit supports embedding cache metadata into pushed images so subsequent builds can pull cache remotely:

# Build and push with inline cache metadata
docker buildx build \
  --cache-to type=inline \
  --push \
  -t registry.example.com/myapp:latest .

# On the next CI run, pull cache from the registry
docker buildx build \
  --cache-from type=registry,ref=registry.example.com/myapp:latest \
  -t registry.example.com/myapp:v2 .

For larger projects, registry-based cache with a dedicated cache image offers better granularity:

docker buildx build \
  --cache-to type=registry,ref=registry.example.com/myapp:buildcache \
  --cache-from type=registry,ref=registry.example.com/myapp:buildcache \
  --push \
  -t registry.example.com/myapp:latest .

Heredoc Syntax

BuildKit 1.4 introduced heredoc support in Dockerfiles, eliminating long chains of && in RUN instructions:

# syntax=docker/dockerfile:1
FROM ubuntu:24.04

RUN <<EOF
apt-get update
apt-get install -y curl git
rm -rf /var/lib/apt/lists/*
EOF

You can also write inline files:

COPY <<EOF /etc/nginx/conf.d/default.conf
server {
    listen 80;
    location / {
        proxy_pass http://app:3000;
    }
}
EOF

This makes Dockerfiles significantly more readable when you need multi-line scripts or configuration files.

Practical Recommendations

Start with cache mounts. They are the lowest-effort, highest-impact BuildKit feature and require only adding a --mount flag to existing RUN instructions. Then audit your Dockerfiles for any ARG that carries a secret and switch to secret mounts. Finally, structure multi-stage builds so independent work happens in separate stages, letting BuildKit parallelize automatically.

The # syntax=docker/dockerfile:1 directive at the top of your Dockerfile ensures you always get the latest stable Dockerfile syntax, including all the features discussed here. Make it the first line of every Dockerfile you write.