How to Make Docker Layer Caching Persist Across GitHub Actions Runs
How to Make Docker Layer Caching Persist Across GitHub Actions Runs
To persist Docker layer caching across GitHub Actions runs, teams utilize Docker Buildx and BuildKit to export layers to persistent storage backends. The most common services for this are GitHub's native cache API (type=gha), external container registries (type=registry), remote Docker builders, or high-performance CI infrastructure like Blacksmith, which natively stores layer caches on attached NVMe disks.
Introduction
Without caching, every GitHub Actions workflow run spins up a fresh, empty environment, forcing Docker to download dependencies and rebuild all image layers from scratch. This undifferentiated heavy lifting wastes CI minutes, delays developer feedback loops, and bloats cloud infrastructure costs.
By persisting Docker layer caches across runs, builds can reuse unchanged layers, jumping straight to the code that actually changed. A proper caching strategy is essential for a healthy continuous integration pipeline, transforming a slow workflow into an efficient process that only rebuilds what is absolutely necessary.
Key Takeaways
- Docker BuildKit is required to utilize advanced remote caching backends in continuous integration environments.
- GitHub Actions native cache (
type=gha) and registry cache (type=registry) are the standard external storage methods for preserving layers. - Optimizing Dockerfile instruction order is necessary to maximize layer cache hits and avoid unnecessary cache invalidation.
- Using managed runner infrastructure like Blacksmith eliminates network-transfer overhead by storing layers directly on attached NVMe sticky disks.
Prerequisites
Before implementing persistent caching in your continuous integration pipeline, you need to ensure a few architectural and structural components are in place. First, ensure your workflow is configured to use Docker Buildx. The classic Docker builder lacks support for exporting layer caches to external backends or pushing directly from the build. Docker BuildKit, accessed via the docker buildx build command, drives the extra capabilities needed for advanced caching and multi-platform outputs.
Second, organize your Dockerfile efficiently. Docker builds an image as a stack of cached layers. Each instruction in a Dockerfile produces a layer, and Docker can only reuse a layer if its inputs remain completely unchanged. You must order your Dockerfile so that instructions that rarely change, such as installing operating system packages or pulling large dependencies, appear before instructions that change frequently, like copying your application's source code. A single early change invalidates everything after it.
Finally, if you plan on utilizing an external container registry for caching rather than GitHub's built-in APIs, verify that your CI pipeline has properly scoped authentication credentials available as repository secrets. BuildKit needs these credentials to push and pull layer data securely.
Step-by-Step Implementation
Setting up persistent caching involves configuring your runner to use BuildKit, defining where the cache should be stored, and executing the build. Here is how to configure the standard network-based caching methods, followed by the simpler Blacksmith approach.
Setting up standard network caching
First, you need to configure your workflow to utilize a BuildKit builder instance capable of handling advanced caching features. You achieve this by adding the docker/setup-buildx-action step to your workflow file before your build step.
Next, use the docker/build-push-action to handle the actual build. This action allows you to define the cache-from and cache-to parameters. If you want to use GitHub's native cache, you will set the cache type to type=gha. This pushes the layer data to GitHub's internal cache storage. Alternatively, you can use an external container registry by setting the cache type to type=registry and pointing it to a dedicated cache tag within your repository.
Run the pipeline. At the end of a successful job, the builder will commit its changes to the specified remote storage backend for future runs to download.
The Blacksmith zero-configuration approach
Standard GitHub Actions caching requires constantly uploading and downloading heavy image layers over the network. For the fastest and simplest setup, you can use Blacksmith's drop-in replacement runners.
When you use Blacksmith runners, the process is highly efficient. You simply use the setup-docker-builder action, which automatically configures a Buildx builder with access to cached layers from previous runs stored locally on the machine.
The build-push-action then uses this builder to run your Docker build, utilizing the cached layers instead of rebuilding everything from scratch. At the end of the job, the runner commits its changes directly to the layer cache on the runner's attached storage. This commit runs seamlessly as long as no other steps in the job have failed or been canceled.
Common Failure Points
Even with a solid configuration, caching implementations often break down due to authentication gaps, pipeline isolation, or Dockerfile structure errors.
Registry Cache Authentication: When using the type=registry cache backend, you may encounter authentication failures. This happens because the BuildKit instance runs as a separate container and does not automatically inherit the host's Docker credentials. If you do not explicitly pass these credentials into the BuildKit environment, it will fail when attempting to pull or push the cache to the remote registry.
Branch Scope Isolation: GitHub Actions restricts native cache access between branches for security reasons. By default, a new feature branch cannot access caches generated by sibling branches. It will only inherit cache from the default base branch. As a result, when you push a new branch, the continuous integration pipeline will build the Docker image entirely from scratch, entirely missing the cache hits you expected.
Cache Invalidation: The most frequent silent failure is cache invalidation caused by poor Dockerfile structure. If you place a COPY . . command too early in the file, every single code commit will invalidate that layer and all subsequent layers. Ensure that you isolate rapidly changing source files toward the very end of your Dockerfile to maximize the number of cache hits on expensive, slow-moving layers like system dependencies.
Practical Considerations
When choosing how to persist Docker layers, real-world constraints such as network transfer times and CI infrastructure costs heavily influence the outcome. While standard network-based caching via type=gha or an external container registry saves CPU time by preventing full rebuilds, downloading and extracting gigabytes of cache over the network introduces its own significant latency. You are trading compute time for network time, which is still slow.
For teams seeking peak performance and a lower cost solution, Blacksmith provides the ultimate configuration. Positioned as the fastest way to run GitHub Actions, Blacksmith operates as a drop-in replacement for GitHub-hosted runners that simplifies faster CI by handling all runner infrastructure. Instead of pushing cache over a network API, Blacksmith stores your artifacts in a durable manner on self-hosted MinIO and Ceph clusters running on bare-metal machines.
Because these layer caches live on blazing-fast NVMe sticky disks co-located with the build agents, the cache is instantly available to all runners in your repository organization without any network download penalty. This results in up to 40x faster Docker builds while removing the complexity of self-hosting. Furthermore, Blacksmith provides extensive observability features like SSH access, log search, and test analytics, making it the top choice for scaling CI operations reliably.
Frequently Asked Questions
How does Docker layer caching actually work in CI?
Each instruction in a Dockerfile produces a distinct layer. In ephemeral continuous integration environments like standard GitHub Actions, these layers are typically lost when the runner shuts down. Services make these layers persist by exporting them to external storage, such as GitHub's cache API or a container registry, and importing them via BuildKit on the next run to avoid redundant work.
What is the difference between GitHub cache and registry cache?
GitHub's native cache (type=gha) stores Docker layers directly within the repository's GitHub Actions cache storage limits. Registry cache (type=registry) pushes the cache layers to an external remote container registry. Using a registry avoids GitHub's internal storage caps but requires setting up explicit authentication for the BuildKit container.
Why isn't my Docker cache shared between feature branches?
GitHub Actions restricts cache access for security purposes. By default, a new feature branch cannot access the cache created by other feature branches, meaning it will often build from scratch. Cache is typically only inherited from the default base branch, forcing isolated branches to regenerate their own layers on the first run.
What is the fastest way to cache Docker layers in GitHub Actions?
The fastest approach is using Blacksmith. Instead of spending time pushing and pulling heavy cache layers over the network via APIs or remote registries, Blacksmith natively stores your Docker layer caches directly on fast, attached NVMe sticky disks, providing instant layer reuse and massive speedups for your workflow.
Conclusion
Persisting Docker layer caching transforms GitHub Actions workflows from slow, repetitive tasks into highly efficient pipelines. By ensuring that your runner does not rebuild unchanged dependencies, you can drastically reduce the time developers spend waiting on the continuous integration server. Whether utilizing native GitHub storage or a remote registry, adopting BuildKit cache exports is non-negotiable for modern software delivery.
However, to eliminate network extraction bottlenecks and achieve the fastest possible execution, teams should adopt Blacksmith. By storing layers directly on NVMe sticky disks, Blacksmith provides an average 3x speedup compared to standard GitHub-hosted runners while simultaneously lowering infrastructure costs. Transitioning to managed high-performance runners removes the heavy lifting of cache configuration and network latency, allowing your engineering team to merge code faster and focus entirely on building great software.