BuildKit is the default Docker builder, and it’s a huge upgrade for CI.
Here’s how you can leverage it in GitHub Actions:
name: Build with BuildKit
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: true
tags: your-dockerhub-username/your-image-name:latest
# This is where BuildKit magic happens
builder: default
This workflow sets up buildx, logs into Docker Hub, and then uses the docker/build-push-action to build and push an image. The key here is builder: default. When setup-buildx-action runs, it registers a docker-container builder by default, which uses BuildKit.
Why BuildKit is a Game Changer for CI
The core problem BuildKit solves is making Docker builds faster, more efficient, and more reliable, especially in ephemeral CI environments. Traditional Docker builds are monolithic: each instruction in a Dockerfile might create a new layer, and if any part of that layer changes, the entire subsequent build cache is invalidated, forcing a rebuild from that point onward. BuildKit breaks this down.
BuildKit analyzes your Dockerfile and its build context to create a directed acyclic graph (DAG) of build steps. It can parallelize independent build steps, cache layers intelligently across different build contexts, and even perform some operations remotely.
Key BuildKit Features in Action
-
Advanced Caching: BuildKit’s cache is much more granular. It can cache intermediate build stages, dependencies, and even specific files within your build context. This means if only a single dependency changes, only that specific part of the build graph needs to be re-executed, drastically cutting down build times.
Consider this:
FROM golang:1.19 as builder WORKDIR /app COPY go.mod go.sum ./ RUN go mod download # This step is heavily cached by BuildKit COPY . . RUN go build -o myapp . FROM alpine:latest COPY --from=builder /app/myapp /myapp CMD ["/myapp"]In the example above,
go mod downloadis a separate stage. If yourgo.modorgo.sumfiles haven’t changed, BuildKit will happily reuse the downloaded modules from its cache, even if your application source code (.) has changed. This is a massive win for CI. -
Parallel Execution: BuildKit can execute independent build stages or commands in parallel. If you have multiple
RUNcommands that don’t depend on each other, BuildKit can spin them up concurrently, utilizing your CI runner’s CPU cores more effectively.FROM ubuntu RUN apt-get update && apt-get install -y --no-install-recommends \ package1 \ package2 \ && rm -rf /var/lib/apt/lists/* RUN apt-get update && apt-get install -y --no-install-recommends \ package3 \ package4 \ && rm -rf /var/lib/apt/lists/*BuildKit can potentially run these two
RUNcommands in parallel if the builder environment supports it. -
Multi-Platform Builds: BuildKit makes building images for different architectures (e.g.,
amd64,arm64) much simpler and more efficient. Thedocker/setup-buildx-actionautomatically sets up abuildxbuilder that can handle multi-platform builds.# In your GitHub Actions workflow - name: Build and push multi-platform image uses: docker/build-push-action@v4 with: context: . file: ./Dockerfile push: true tags: your-dockerhub-username/your-image-name:latest platforms: linux/amd64,linux/arm64 builder: defaultWhen you specify multiple
platforms, BuildKit will build the image for each architecture and then create a manifest list (or "fat manifest") that points to the correct image for the target architecture. -
Remote Caching: BuildKit supports pushing and pulling build cache to/from remote storage, such as Docker Hub, AWS ECR, or Google Container Registry. This allows you to share build cache between different CI runs or even between different CI runners, further accelerating builds.
To enable this, you’d typically pass cache export/import options to the
build-push-action:- name: Build and push Docker image with remote cache uses: docker/build-push-action@v4 with: context: . file: ./Dockerfile push: true tags: your-dockerhub-username/your-image-name:latest cache-from: type=registry,ref=your-dockerhub-username/your-image-name:buildcache cache-to: type=registry,ref=your-dockerhub-username/your-image-name:buildcache,mode=maxHere,
cache-fromtells BuildKit to pull cache from the specified registry reference, andcache-totells it to push the generated cache back to that reference.mode=maxensures that all possible cache is exported.
The "Secret" of Layer Re-use
What most people miss about BuildKit’s caching is how it handles the build context. It doesn’t just hash the entire COPY command. Instead, it can track changes to individual files within the context. If you COPY . . and only one file in your project changes, BuildKit is smart enough to only invalidate the cache for that specific file’s addition to the image layer, not the entire COPY operation. This is a subtle but critical difference that unlocks significant speedups.
The next step is integrating BuildKit’s advanced features like multi-stage build optimization and finer-grained cache control into your deployment pipeline.