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

  1. 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 download is a separate stage. If your go.mod or go.sum files 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.

  2. Parallel Execution: BuildKit can execute independent build stages or commands in parallel. If you have multiple RUN commands 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 RUN commands in parallel if the builder environment supports it.

  3. Multi-Platform Builds: BuildKit makes building images for different architectures (e.g., amd64, arm64) much simpler and more efficient. The docker/setup-buildx-action automatically sets up a buildx builder 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: default
    

    When 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.

  4. 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=max
    

    Here, cache-from tells BuildKit to pull cache from the specified registry reference, and cache-to tells it to push the generated cache back to that reference. mode=max ensures 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.

Want structured learning?

Take the full Buildkit course →