`docker buildx --cache-to` and `--cache-from` Are the Flags You Need When CI Keeps Rebuilding the World
A practical guide to Docker Buildx cache configuration for developers who want faster image builds in CI instead of re-downloading and re-compiling everything on every run.
Why this matters: a surprising number of slow CI pipelines are not slow because your code is huge. They are slow because your build cache strategy is nonexistent.
Docker Buildx supports explicit cache import and export with --cache-from and --cache-to. If your CI keeps rebuilding dependencies, re-running expensive package install layers, or recompiling the same stack over and over, this is one of the first places to look.
The problem with default assumptions
A lot of teams assume Docker caching will “just happen.” That is only partially true. Local builds often benefit from local layer cache, but CI runners are usually ephemeral. That means each run starts colder than developers expect.
If you do not explicitly export cache somewhere reusable, then every fresh runner behaves like it has never seen your project before.
That gets expensive fast.
The basic pattern
Buildx lets you import cache:
docker buildx build \
--cache-from type=registry,ref=ghcr.io/example/app:buildcache \
--cache-to type=registry,ref=ghcr.io/example/app:buildcache,mode=max \
-t ghcr.io/example/app:latest .The exact backend can vary, but the idea stays the same:
- pull prior cache
- build
- push updated cache
That is how you stop every CI run from behaving like day one.
Why registry-backed cache is common
When your runner is disposable, a remote cache destination is often the most practical choice. A registry-backed cache works well because it travels with your pipeline and does not depend on the runner’s local disk surviving.
This is especially useful for:
- dependency-heavy Node images
- Python images with compiled packages
- multi-stage builds
- polyglot stacks with expensive package layers
Layer order still matters
Caching flags do not rescue a badly ordered Dockerfile.
You still want to:
- copy dependency manifests early
- install dependencies before copying frequently changing app code
- keep the most stable expensive layers higher up
Example idea:
FROM node:22-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm buildIf you copy the whole repo before dependency install, you invalidate more cache than necessary.
Good Dockerfiles and Buildx cache settings work together.
Why this is often the biggest CI win per minute spent
Developers love deep CI refactors, but sometimes the highest ROI move is simpler:
- export cache
- import cache
- fix layer ordering
That can cut build time dramatically without changing your application at all.
When to use it
Reach for Buildx cache controls when:
- your image builds are expensive
- runners are ephemeral
- local builds are much faster than CI
- package installation dominates pipeline time
Those symptoms nearly always justify checking cache strategy before blaming the app itself.
Final recommendation
If CI keeps rebuilding the world, stop assuming Docker cache is automatic in ephemeral runners. Use Buildx with explicit --cache-from and --cache-to, keep your Dockerfile cache-friendly, and treat build cache as part of pipeline design rather than as a happy accident.