Build1 distinct publisher3 min readUpdated
A dev.to teardown argues the 11-minute CI build that runs in 20 seconds locally is a cache-miss problem, and that one run with --progress=plain will prove it before you pay for bigger runners.
The Engineer · Build desk
Compiled by The EngineerSomething wrong?How this is made
A post on dev.to makes a claim worth testing on your own pipeline: when a Docker build takes 11 minutes in CI and 20 seconds on a laptop, the base image is almost never the cause [1]. That gap is roughly 33x [19], and it is the kind of number that gets teams shopping for faster runners instead of reading their build log.
The mechanism is unglamorous. Your local daemon keeps every intermediate layer on disk between builds; a hosted runner is a fresh VM, so `docker build` has nothing to compare against and every `RUN` re-executes, including the three-minute `npm ci` or `pip install` nobody thinks about [2][5]. And a `COPY . .` placed above your dependency install throws away whatever cache you did manage to restore, because BuildKit invalidates a layer when its inputs change and every layer after it [3][9].
Both failures are visible in a single run. The author's diagnostic is `docker build --progress=plain -t myapp:ci . 2>&1 | grep -E 'CACHED|DONE'`: a warm local build produces a wall of `CACHED` lines, a cold runner produces almost none [6][7]. That difference, not the distro, is the eleven minutes [1].
This is also why the popular non-fix does nothing. Swapping `node:22` for `node:22-alpine` barely moves build time, because image size affects push and pull while cache hits affect build [8]. Two different bills.
The ordering fix is mechanical: things that change rarely go up top, things that change every commit go at the bottom [9]. In the Node shape that means a `deps` stage that copies only `package.json` and `package-lock.json`, runs `npm ci --omit=dev`, then a runtime stage that copies `node_modules` from `deps` and does `COPY . .` last [10]. Python is identical with `requirements.txt` and `pip install` above the source copy; with Poetry or uv, copy the manifest and lockfile together, since installing from a manifest without its lock defeats the reproducible layer [11]. Two things bite people here: a `.dockerignore` that misses `.git` or `node_modules` makes your build context change every run for reasons unrelated to your code, and anything writing a timestamp or build ID into an early layer invalidates everything below it permanently [12][13].
Only then does a cache backend earn its keep. On GitHub Actions the least-effort route is `cache-from: type=gha` with `cache-to: type=gha,mode=max` behind `setup-buildx-action@v3` and `build-push-action@v6` [14]. `mode=max` matters for multi-stage builds because it exports intermediate stage layers, and the expensive `npm ci` lives in a stage that never ships [15]. The cost is size: per the same post, GitHub's Actions cache is capped per repository at 10 GB as of mid-2026 with least-recently-used eviction, so a `mode=max` cache on a busy monorepo can push your other caches out [16]. A registry cache avoids that ceiling and works on any provider [17]. The post also names Depot as a managed builder that holds a persistent BuildKit cache volume across runs, skipping the export/import round trip [18].
What to watch: run the grep before and after reordering and count `CACHED` lines, not wall-clock minutes, since a noisy runner will hide a real improvement. Then check whether your `mode=max` cache is quietly evicting your test caches [16]. If `COPY . .` still sits above the install, no backend will save that build [3].
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
If a Docker build is fast locally and slow in CI, the base image is almost never the problem; the cache-miss difference, not image size or base distro, accounts for the eleven minutes.
CI runners are ephemeral and start with an empty layer cache unless a cache backend is explicitly wired up.
Locally the daemon keeps every intermediate layer on disk between builds; a hosted runner is a fresh VM, so docker build has nothing to compare against and every RUN re-executes from scratch, including a three-minute npm ci or pip install.
Switching from node:22 to node:22-alpine barely moves build time, because image size affects push and pull time while cache hits affect build time.
BuildKit invalidates a layer when its inputs change, and every layer after it; the rule is that rarely changing things go up top and things that change on every commit go at the bottom.
Anything that writes a timestamp or build ID into an early layer will invalidate everything below it permanently; such steps belong in the last stage.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Mechanism well specified, outcomes unmeasured
The causal story (ephemeral runners start with an empty layer cache; BuildKit invalidates a changed layer and everything after it) is stated precisely and comes with reproducible artifacts: an exact diagnostic command, a complete corrected multi-stage Dockerfile, and working cache-from/cache-to configuration for both the GitHub and registry backends. What is missing is measurement: no before/after build times for the prescribed fix, no benchmark behind the 11-minute versus 20-second framing, and no citation for the dated 10 GB Actions cache quota. Single publisher, single author, no corroborating documentation in the cluster.
No adoption data supplied
The sole source is a how-to post. It reports no deployments, usage numbers, repository counts, telemetry, or customer evidence for the pattern it prescribes or for the tools it names (type=gha backend, registry cache, Depot). The one ecosystem datapoint present — that the type=gha backend was rewritten against GitHub's newer cache service and older Buildx versions hit a retired API — is a version note, not a measure of uptake, so no adoption score can be derived without inferring facts the cluster does not contain.
Mildly overstated framing over sound mechanics
The underlying technical content is unusually restrained for the genre — it flags tradeoffs (mode=max cache size versus the repository quota), corrects a common misconception in the reader's favour, and explicitly limits cache mounts to persistent builders rather than selling them as the fix. The overstatement is rhetorical and narrow: 'almost never the problem', 'no cache backend on earth will save that build', and a headline 33x gap presented without measurement, plus one unhedged named vendor. Slightly positive rather than aligned.
Mostly vendor-neutral with one undisclosed product plug
Advice is dominated by first-party, no-cost mechanisms the reader already owns: Dockerfile reordering, .dockerignore hygiene, the built-in GitHub Actions cache backend, and a provider-agnostic registry cache. Against that, the post inserts a single commercial recommendation — Depot as 'the' managed builder — with no disclosure, no alternatives named, and no pricing, on a syndication platform where promotional posts are common. Moderate incentive exposure, concentrated in one paragraph rather than shaping the technical argument.
Checkable but uncorroborated single source
Confidence is capped by the cluster shape: one publisher, one author, no second voice on any claim and no adoption measure at all. It is lifted by verifiability — the ordering rules and cache-backend configuration are standard, internally consistent, and a reader can confirm or refute the central diagnosis in a single build run. The dated 10 GB quota is held as insufficient rather than folded into the score.
build
The npm audit that works because it never installs the package1 distinct publisher
build
A file-copy Allure adapter for Katalon, and the history IDs that make retries useful1 distinct publisher
build
Split Flutter CI from CD, or pay macOS rates on every pull request1 distinct publisher
build
Eleven breaks in one sitting: where a non-developer's agent gateway install actually dies1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 16, 2026