Skip to main content

Overview

Now that you have a Blacksmith runner, you can take advantage of our NVMe-backed cache to persist your Docker layers across CI runs. To enable Docker layer caching, you’ll use two Blacksmith actions in your GitHub Actions workflow file. This allows your Docker builds to reuse cached docker layers from previous runs, and only rebuild the layers that have changed. Our customers have reported 2x to 40x improvements in build times due to this change.
Diff Example
Any external caching that was configured with cache-from and cache-to directives can now be removed. Exported caches (type=gha, type=registry, type=inline) serialize layer blobs on every build and never include the contents of RUN --mount=type=cache directories; Blacksmith keeps the builder’s state (layers and cache mounts) on a persistent disk instead, so there is nothing to import or export: your builds skip the latency of downloading and uploading the entire cache blob on every run. 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. The required cache-key input identifies the layer cache this build uses; builds with the same key share cached layers across runs. A good default is the Dockerfile being built (e.g., services/api/Dockerfile); see scoping guidance below for when builds should share a key.
When using useblacksmith/build-push-action without useblacksmith/setup-docker-builder, the runner will use the default builder configured in your environment. However, this builder will not leverage Blacksmith’s Docker layer caching nor will it report Docker analytics to the Blacksmith control plane.
For a deeper treatment of how Docker build caching works (layer invalidation, cache mounts, backend trade-offs, and measurements), see our blog post on the physics of Docker build caching.

Basics

Not using the docker/build-push-action?

If you’re not using the docker/build-push-action in your workflow, but are instead calling Docker commands directly or are using the docker/bake-action, you can still cache your Docker layers by setting up a Blacksmith builder before interacting with Docker. This builder will be hydrated with the layer cache from previous runs and will commit the updated layer cache at the end of the job.
Diff Example

Garbage collection

The layer cache is kept in check by BuildKit’s native garbage collection with a time-based policy: layers that haven’t been used for 8 days are automatically cleaned up, while actively used layers are kept regardless of total cache size. No configuration is needed.

Getting the most out of the layer cache

A persistent builder makes caching fast, but how much of each build is cached still depends on your Dockerfile and workflow. The following practices have the largest measured impact:

Scope the cache to one build workload

Builds only benefit from sharing a cache when they actually reuse each other’s layers. Unrelated images building against one shared cache evict each other’s state and can be slower than not caching at all. The required cache-key input on setup-docker-builder makes this scope explicit. The unit of scoping is the builder: one cache backs everything a builder instance builds. When each job builds one Dockerfile — the common case — that means one key per Dockerfile, and the Dockerfile’s path is a good key. When a single job builds several related Dockerfiles behind one builder (variants of one image, a base image plus images derived from it), they share that job’s key; name it after the image set rather than one Dockerfile. Related images that reuse each other’s layers can share a key on purpose — it is genuinely unrelated builds that should be kept on separate keys. Use the same key for the same workload across workflows so, for example, release builds reuse layers from CI builds of the same image.

Order layers by change frequency

A changed instruction invalidates its layer and every layer after it. Put expensive, stable steps early and frequently changing content late. The most common form is manifest-first ordering: copy the dependency manifest, install dependencies, and only then copy the source, so the install layer is only rebuilt when the manifest changes.
Example

Use cache mounts for package managers and compilers

RUN --mount=type=cache directories persist package-manager and compiler state (Go’s build cache, cargo’s registry and target/, pip’s wheels, pnpm’s store) across builds, so a step that does have to re-run is incremental instead of from scratch. Because Blacksmith persists the whole builder disk, these mounts survive between CI jobs; with export-based backends (type=gha, type=registry) they are always empty on a fresh runner.
Example

Push from the builder instead of loading into the daemon

If your job pushes the image to a registry, use push: true on the build action rather than load: true followed by docker push. load: true exports the image into the local Docker daemon and then reads it back for the push, handling every byte twice; push: true streams the image from the builder straight to the registry.
Example
If you need to inspect the pushed image (e.g., check its size), use docker buildx imagetools inspect user/app:latest instead of loading it locally.

How it works

Under the hood, your Docker layer caches are stored on sticky disks.

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 cache your Docker layers

When a GitHub Action job uses the Blacksmith Docker actions, the process works as follows:
  1. The setup-docker-builder action configures a buildx builder with access to cached layers from previous runs
  2. The build-push-action then uses this builder to run your Docker build, leveraging the cached layers instead of rebuilding everything from scratch
  3. At the end of the job, the runner commits its changes to the layer cache for future runs. This commit only runs if no other steps in the job have failed or been canceled.
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. Note that the layer cache is separate from Docker container caching, which caches the daemon’s image store (/var/lib/docker) used by docker pull, docker run, service containers, and container: jobs.

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.
Diff Example
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.

Merging images into a multi-arch manifest

After building separate images for each architecture, you can merge them into a single multi-arch manifest using Docker’s manifest commands:
Diff Example
For registries that require explicit annotation of architectures:
Diff Example

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.
Diff Example
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

Docker layer caching executes 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.

Pricing

Docker build caching is powered by sticky disks and is charged at the same rate of $0.50/GB/mo. For pricing details, please visit our pricing page.

FAQ

Docker layer caches are stored on sticky disks, with a separate sticky disk created per cache-key. The sticky disk is automatically evicted after 7 days of inactivity. Each Docker build updates the “last used” timestamp on the sticky disk, so as long as you run at least one Docker build within a 7-day window, your layer cache will remain available.
Yes, useblacksmith/build-push-action@v1 has been deprecated. Please move to the newer approach laid out above. If you were using setup-only refer to this section.
Users can login to the Blacksmith dashboard and navigate to the Usage & Billing page to get a breakdown of their current usage.