Skip to main content

Blacksmith and Depot Colocate Docker Layer Cache on NVMe: What 40x Faster CI Builds Mean for Self-Hosted Build Pipelines

9 min readDora NodaDora Noda
Share
On this page

Every team that has watched a CI job re-download and re-extract the same Docker layers it built ten minutes ago knows the specific flavor of frustration: the code didn't change, the dependency lockfile didn't change, and the build still spends ninety seconds pulling node_modules out of a cache stored somewhere on the other side of the internet. In 2026, two CI vendors turned "stop doing that" into a headline number. Blacksmith advertises up to 40x faster Docker builds. Depot claims up to 20x. Both get there with the same trick: stop treating the Docker layer cache as something you fetch, and start treating it as something that's already sitting on the disk next to the build.

The punchline, stated plainly

The mechanism behind both numbers isn't a smarter cache algorithm — it's disk locality. A cache stored in a registry or an S3 bucket has to travel over the network on every build; a cache stored on an NVMe volume physically attached to the machine running the build doesn't. That's the entire trick, and it means the 40x and 20x numbers aren't really vendor magic. They're the payoff of not tearing down the build machine's disk between runs — something any operator running dedicated build hardware already gets for free, and something a rented, ephemeral-runner-per-job CI vendor structurally can't offer without building exactly the sticky-disk infrastructure Blacksmith and Depot are now selling as a premium feature. If your build pipeline already runs on machines you own, the interesting question isn't "should I buy this" — it's "am I already leaving this speedup on the table by re-provisioning a fresh disk per build anyway."

Why network-bound caching is slow in the first place

Docker's BuildKit supports a few different caching strategies, and they don't perform anywhere close to equally:

  • Registry cache — layers get pushed to and pulled from a container registry (often the same one storing your final images). It requires no extra infrastructure, but every cache hit means a network round-trip to fetch a layer that might be hundreds of megabytes.
  • S3/blob cache — similar shape, similar problem. Depot's own engineering writeup on why they moved away from this approach is blunt about it: registry and blob caches are slow specifically because of the network latency of transferring large layer archives, even when the storage is in the same cloud region as the build.
  • Local cache mounts on persistent disk — when the build machine itself keeps a persistent NVMe volume across runs, BuildKit reads and writes cache directly to local disk. No network hop, no serialization to a blob format and back. Teams report cache mounts cutting dependency-install steps by 10x or more on their own — before either vendor's org-wide sharing trick even enters the picture.

That 10x baseline is what a persistent local disk buys anyone. The two vendors' 20x-40x numbers come from pushing that same idea further: sharing one persistent cache across every runner and every branch in a repository, not just across sequential runs on the same ephemeral machine.

How Blacksmith and Depot actually build it

The two vendors solve the "share a persistent disk across ephemeral machines" problem differently, and the difference is worth knowing before you copy either approach onto owned hardware.

Blacksmith pins Docker layers to sticky NVMe disks that are shared across every runner in an organization's repository — not scoped to a single branch or a single ephemeral VM the way GitHub's default actions/cache is. A build on feature/foo can warm a cache that a build on main then reuses, because the disk itself is the shared resource, not a blob keyed to a specific branch. Blacksmith prices its runners at $0.004/minute; layered on top of GitHub's 2026 platform fee of $0.002/minute (which applies to every third-party runner provider, not just Blacksmith), the blended cost lands around $0.006/minute — still cheaper than GitHub's own $0.008/minute Linux runners, before counting the build-time savings at all.

Depot takes a heavier-infrastructure route: a Ceph-backed distributed storage cluster persists the NVMe cache independently of any single build VM, then reattaches it to whichever machine picks up the next build. Depot's builders also ship on notably beefier hardware than typical CI runners — 16 vCPUs, 32 GB of memory, and a persistent 50+ GB NVMe cache volume per builder — and GitHub Actions runners sit on the same colocated infrastructure as the container builds themselves, cutting cross-service network hops entirely.

Different architectures, same underlying bet: decouple the cache's lifetime from the build machine's lifetime, and put the two on the same rack (or the same disk) so retrieving the cache costs microseconds instead of the round-trip time to a registry.

The numbers aren't just marketing copy, either. PostHog's own writeup of switching to Depot reports cutting a single Docker build from 193 minutes down to 3 minutes 26 seconds — a 55x improvement on their specific, dependency-heavy monorepo — and roughly 44 days of cumulative build time saved over a two-week window across about 118,000 GitHub Actions jobs. That's the effect of colocated NVMe caching compounding across an org's real job volume, not a synthetic benchmark.

The same argument applies to buildpacks, not just Dockerfiles

Everything above is framed around Docker/BuildKit because that's where Blacksmith and Depot compete, but a git-push PaaS that builds from source with Cloud Native Buildpacks rather than a tenant-supplied Dockerfile has the identical cache-locality problem, one layer up the stack. CNB's lifecycle already separates the detect, build, and export phases specifically so that unchanged dependency layers can be skipped on a rebuild — but that skip only pays off if the layer cache from the previous build is sitting where the next build can find it. Run each buildpack build on a fresh, disposable node and you've thrown away the exact optimization the buildpack spec was designed around, for the same reason a Dockerfile build loses its layer cache on a fresh runner: the cache lived on a disk that no longer exists. The fix is the same fix — a persistent volume tied to the build-node pool, not the build job — whether the thing running inside it is docker build or pack build.

What owning the hardware already buys you

This is where the story changes for a self-hosted, git-push PaaS running its own build pipeline on owned Hetzner capacity instead of renting per-minute CI compute. Blacksmith and Depot had to build sticky-disk infrastructure because their underlying compute is still fundamentally ephemeral — a customer's build lands on whichever runner is free, and without deliberate engineering, that runner's local disk is gone the moment the job ends. A platform running its own Cluster API-managed fleet of dedicated build nodes doesn't have that problem to begin with, provided it doesn't accidentally reintroduce it by treating build nodes like disposable pods.

The concrete architecture looks like this:

  1. A labeled build-node pool, distinct from the general-purpose worker pool a Cluster API fleet uses for tenant workloads — machines whose local NVMe is treated as durable state, not scratch space that gets wiped on every pod reschedule.
  2. A persistent volume (or a plain hostPath) mounted into the build step's container, holding BuildKit's cache directory, that survives across builds on that node rather than getting provisioned fresh per job.
  3. Sticky scheduling keyed to the repository (or the tenant) — the same affinity Kubernetes already supports for stateful workloads, applied to build jobs so that repeat builds for the same app land on a node that's already warmed its cache, instead of round-robining across the pool and cold-starting the cache on every third build.

None of these three pieces requires new infrastructure a Cluster-API-based platform doesn't already have primitives for — persistent volumes and node affinity are both things Kubernetes does natively. What it requires is deciding to treat build-node local disk as a durable, cache-bearing resource instead of defaulting every build job to "any free node, any fresh disk," which is the same default-ephemeral posture that forced Blacksmith and Depot to build sticky-disk infrastructure as a selling point in the first place.

The trade-offs of owning it

Colocated NVMe caching isn't free of downsides just because the hardware is already paid for:

  • Elasticity goes down. Pinning a repository's builds to a specific node (or small set of nodes) to preserve cache locality means that node's capacity, not the whole pool's, bounds how fast that repo's builds can run under load. A platform has to size its build-node pool for peak per-tenant concurrency, not just aggregate throughput.
  • Cold cache is still cold. None of this changes the very first build for a brand-new app or a dependency-lockfile change big enough to invalidate most layers. The 10x-40x numbers describe warm-cache reuse, not a floor on every build.
  • Cache eviction needs an actual policy. A cache that never gets garbage-collected is a disk-full incident waiting to happen. Blacksmith and Depot both operate this at fleet scale for thousands of customers; a self-hosted platform building the same thing on owned Hetzner nodes needs its own LRU-or-similar eviction logic, not just "let it grow."
  • Multi-tenant isolation is a real decision, not a default. Blacksmith shares its sticky disk across every runner in an organization — reasonable when the org is one company's own repos. A multi-tenant PaaS caching multiple customers' builds on the same physical node has to decide explicitly whether cache is scoped per-tenant (safer, less cache-hit potential across similar stacks) or shared more broadly (faster on average, requires trusting that a cache poisoning or supply-chain issue in one tenant's build can't leak into another's).

None of these are reasons to skip colocated caching — they're the actual engineering work "put the cache next to the compute" compresses into a one-line pitch when a vendor sells it back to you as a feature.


Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. A build pipeline that already runs on owned Hetzner hardware doesn't need to buy back the cache locality a rented-runner vendor has to re-engineer from scratch. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex