Overview

Blacksmith runners support incremental docker builds with a one-line code change.

With this feature, Docker builds in your GitHub Action workflows can reuse cached layers from previous runs, and only rebuild the layers that have changed. This can significantly reduce the time it takes to build your Docker image. Our customers have reported 2x to 40x improvements in build times due to incremental Docker builds.

Quickstart

To enable incremental Docker builds with Blacksmith runners, all you need to do is change the following line in your GitHub Actions workflow file:

   name: Build and Push Docker Image
-  uses: docker/build-push-action@v3
+  uses: useblacksmith/build-push-action@v1

The useblacksmith/build-push-action action is a drop-in replacement for the docker/build-push-action action and any inputs you provide to the docker/build-push-action action will work with the useblacksmith/build-push-action action.

Once you make this switch, the first Docker run will be an uncached run. Every subsequent run will have the hydrated layer cache mounted into your runners, so you should see several build steps cached from previous runs. With Blacksmith’s incremental docker builds, any external caching that was configured with cache-from and cache-to directives can now be removed. There is no longer a need to upload and download artifacts since your runner will already be hydrated with the relevant Docker layer cache.

How it works

How caching works in Docker builds

When you do a Docker build, each step in your Dockerfile creates a new layer in your Docker image. Without caching, when you make a change to your Dockerfile, Docker will rebuild all the layers in the image, even if only one layer has changed. This can be slow, especially for large Docker images.

However, with caching, Docker can reuse layers from previous builds instead of rebuilding them from scratch. Docker will only rebuild from the layer that has changed and use the cached layers for the rest of the image.

How Blacksmith runners enable incremental Docker builds

When a GitHub Action job uses the useblacksmith/build-push-action, a hydrated layer cache from previous runs will be mounted into the runner. The Docker build triggered by the build-push-action will subsequently be able to use the cached layers, instead of rebuilding everything from scratch. At the end of the job, the runner will commit its changes to the layer cache for future runs.

The Docker layer cache is shared by all runners in a repository, in your organization. In case of several concurrent Docker builds, it may take a few runs until all the builds have their layers committed to the cache. This is in keeping with the Last Write Wins (LWW) policy we enforce in the face of concurrent committers.

Multi-platform builds

Current approach: Using a matrix strategy

You can build multi-platform Docker images on Blacksmith by using GitHub Actions matrix strategy. This approach leverages Blacksmith’s native runners for each architecture to avoid the performance penalties of emulation.

jobs:
  build:
    strategy:
      matrix:
        platform: [amd64, arm64]
        include:
          - platform: amd64
            runner: blacksmith-8vcpu-ubuntu-2204
            docker_platform: linux/amd64
          - platform: arm64
            runner: blacksmith-8vcpu-ubuntu-2204-arm
            docker_platform: linux/arm64
    runs-on: ${{ matrix.runner }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      
      - name: Login to DockerHub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      
      - name: Build and push Docker image
        uses: useblacksmith/build-push-action@v1
        with:
          push: true
          tags: user/app:${{ matrix.platform }}
          platforms: ${{ matrix.docker_platform }}

This approach runs each architecture build on its native hardware - the amd64 build runs on blacksmith-8vcpu-ubuntu-2204 and the arm64 build runs on blacksmith-8vcpu-ubuntu-2204-arm. Each image is pushed with its own tag that includes the architecture.

For ARM builds, this avoids needing to use QEMU to emulate ARM on an amd64 runner, which can be extremely slow.

If you need to merge these separate images into a single multi-arch manifest, the process is more involved and requires additional steps. Please reach out to [email protected] for assistance with creating multi-arch manifests.

Coming soon: Native multi-platform support

In the future, the useblacksmith/build-push-action action will support multi-platform builds natively. You’ll simply need to specify the platforms you want to build for in the platforms input, and Blacksmith will automatically spawn native builders for each platform, eliminating the need for the matrix strategy shown above.

jobs:
  build:
    runs-on: blacksmith-8vcpu-ubuntu-2204
    steps:
    ...
    - name: Build and push Docker image
        uses: useblacksmith/build-push-action@v1
        with:
        platforms: linux/amd64,linux/arm64

When this feature is available, each image will be built on a native builder (i.e., the amd64 Docker build on a blacksmith-8vcpu-ubuntu-2204 and the arm64 build on a blacksmith-8vcpu-ubuntu-2204-arm runner) and automatically merged into a single multi-arch manifest.

Security

Incremental Docker builds execute within the same runners that process your GitHub Actions workflows. This means they automatically inherit all security protections and isolations that are detailed in our security documentation.

The BuildKit daemon in each runner (buildkitd) that powers Docker builds, runs exclusively on a local Unix socket and is not exposed to the public internet.

The Docker layer cache for each repository is stored in a secure Ceph cluster. Every runner gets an ephemeral authentication token that allows it to request and commit artifacts. The runners do not have persistent credentials to the Ceph cluster or direct access to artifacts in the cluster. The Ceph cluster is configured with object-level access controls.