Accelerating CI Pipelines with Docker Buildx and BuildKit Caching

Siddhant Khare - Sep 15 - - Dev Community

Introduction

Efficient CI pipelines are critical to smooth development workflows, and caching plays a significant role in reducing build times. As CI workflows increasingly leverage Docker, understanding how Docker build caching works is essential for optimizing performance, especially in complex projects. In this post, we’ll dive into the fundamentals of Docker build caching, explore the components needed for effective caching with GitHub Actions, and walk through practical examples.

Docker’s v23 update introduced a significant shift in its build architecture, standardizing the use of Buildx as the build client and BuildKit as the backend. This change provides developers with the power to optimize their build processes, yet caching in CI still requires a deliberate setup, particularly when working with Docker Compose. Here, we’ll break down the basics, explain the necessary tooling, and demonstrate how to implement build caching in CI using GitHub Actions.

Docker Build in v23 and Beyond

As of Docker v23, which was released in February 2023, the traditional build system transitioned to a new architecture. The docker build command now defaults to using Buildx as the client and BuildKit as the backend for managing the build process.

Architecture diagram

This shift is important because BuildKit significantly enhances the performance of Docker builds by allowing parallel execution, efficient layer caching, and better resource utilization. To benefit from these improvements, it is crucial to understand how to use Buildx and configure caching effectively, particularly in CI environments.

Why Docker Compose Does Not Automatically Use BuildKit

While the standard docker build command has adopted Buildx and BuildKit, Docker Compose continues to use the traditional build system by default. When running docker compose build, neither Buildx nor BuildKit is utilized unless explicitly configured.

This is important because Docker Compose, a tool frequently used for defining and running multi-container Docker applications, does not take advantage of the caching optimizations offered by BuildKit out of the box. To leverage caching in a CI pipeline, you must configure Docker Compose to use Buildx and BuildKit manually.

Specifying Build Cache Inputs and Outputs

To enable caching, the build cache must be inputted and outputted to a specific location in the file system. Docker provides options for specifying external cache sources and destinations via --cache-from and --cache-to:



docker build --cache-from type=local,src=/path/to/dir --cache-to type=local,dest=/path/to/dir ...


Enter fullscreen mode Exit fullscreen mode
  • --cache-from: Specifies the source of the build cache.
  • --cache-to: Defines where the cache should be exported after the build.

By using these flags, you can share and store cache layers between builds, ensuring faster rebuilds by skipping already cached layers. This setup is especially useful in CI environments where each build typically starts from scratch.

The Need for Buildx and docker-container Driver

However, when running the above command in a local environment, you might encounter the following error:



Cache export is not supported for the docker driver.
Switch to a different driver, or turn on the containerd image store.


Enter fullscreen mode Exit fullscreen mode

This happens because the legacy Docker driver (docker) does not support BuildKit’s cache export features. To resolve this, you need to use the docker-container driver, which fully supports BuildKit’s advanced features. (docs)

To create a new builder that uses the docker-container driver, run:



docker buildx create --name mybuilder --driver docker-container
docker buildx use mybuilder


Enter fullscreen mode Exit fullscreen mode

This sets up a Buildx builder that uses the docker-container driver. Now, you can use the --cache-from and --cache-to options to cache Docker layers effectively.

Example: Building with Buildx and Caching

Once the new builder is created, you can use the following command to build and cache your image:



docker buildx build --builder mybuilder \
  --cache-from=type=local,src=/path/to/dir \
  --cache-to=type=local,dest=/path/to/dir ...


Enter fullscreen mode Exit fullscreen mode

Caching with Docker Compose

As mentioned earlier, Docker Compose does not automatically utilize BuildKit. To achieve caching with Docker Compose, you need to use the docker buildx bake command. Bake is a tool in Buildx that allows multi-step orchestration of Docker builds, including advanced caching options.

Here’s how you can set up Docker Compose to leverage BuildKit caching:



docker buildx bake --file docker-compose.yml \
  --builder mybuilder \
  --cache-from=type=local,src=/path/to/cache \
  --cache-to=type=local,dest=/path/to/cache,mode=max


Enter fullscreen mode Exit fullscreen mode

This approach bypasses the default docker compose build command and uses Buildx’s bake to handle the build process with caching.

Implementing Caching in GitHub Actions

To use Docker build caching in GitHub Actions, you’ll need to set up caching for the build directory, configure Buildx, and run docker buildx bake as part of the CI workflow. Here’s a sample workflow file that illustrates this:



name: CI Build

on:
push:
branches:
- main

jobs:
build:
runs-on: ubuntu-latest

<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout code</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Setup Docker Buildx</span>
  <span class="na">id</span><span class="pi">:</span> <span class="s">buildx</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Cache Docker layers</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/cache@v3</span>
  <span class="na">with</span><span class="pi">:</span>
    <span class="na">path</span><span class="pi">:</span> <span class="s">/tmp/.buildx-cache</span>
    <span class="na">key</span><span class="pi">:</span> <span class="s">docker-build-cache-${{ github.ref }}-${{ github.sha }}</span>
    <span class="na">restore-keys</span><span class="pi">:</span> <span class="pi">|</span>
      <span class="s">docker-build-cache-${{ github.ref }}</span>
      <span class="s">docker-build-cache-</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build with caching</span>
  <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">docker buildx bake --file docker-compose.yml \</span>
      <span class="s">--builder="${{ steps.buildx.outputs.name }}" \</span>
      <span class="s">--cache-from=type=local,src=/tmp/.buildx-cache \</span>
      <span class="s">--cache-to=type=local,dest=/tmp/.buildx-cache,mode=max</span>
Enter fullscreen mode Exit fullscreen mode
Enter fullscreen mode Exit fullscreen mode




Verifying the Cache

Once the workflow completes, you can inspect the contents of the build cache stored in /tmp/.buildx-cache. The cache is typically stored in OCI (Open Container Initiative) image format, with each image layer cached in the blobs directory. The index.json file in the cache contains metadata about the cached layers.

Here’s a snippet of what the index.json file might look like:



{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.index.v1+json",
"digest": "sha256:c6fc9d593e92059b058b0b147be8e51...",
"size": 4193,
"annotations": {
"org.opencontainers.image.ref.name": "latest"
}
}
]
}
Enter fullscreen mode Exit fullscreen mode




Conclusion

In this guide, we covered the essentials of setting up Docker build caching in CI, focusing on Buildx, BuildKit, and GitHub Actions. Docker Compose doesn’t use BuildKit by default, but by utilizing the buildx bake command, you can implement effective caching strategies. While caching might not always result in significant performance improvements depending on the use case, understanding and leveraging Docker’s build architecture will put you in a better position to optimize your CI pipelines.

By following these steps, you can make sure your builds are faster, more efficient, and consistent across environments.

Thank you for reading, and happy optimizing!


For more tips and insights on security and log analysis, follow me on Twitter @Siddhant_K_code and stay updated with the latest & detailed tech content like this.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player