Docker Build Speed Isn’t Magic but Cache Discipline


Slow Docker builds are rarely caused by Docker itself.

More often, they’re the result of accidental cache invalidation.

Docker already caches aggressively by default.
What matters is when that cache gets reused and when it doesn’t.

This post explains how Docker evaluates layers, why builds suddenly become slow, and how a small shift in mindset leads to consistently fast builds.


Docker caching in one sentence

Docker doesn’t cache commands.
It caches layers and reuses them only if nothing above them changes.

Once you understand that, most build issues stop being mysterious.


How Docker actually builds an image

A Dockerfile is evaluated top to bottom.

Each instruction produces a new layer:

FROM python:3.11
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]

Docker decides for each layer:

“Can I reuse a cached version of this, or do I need to rebuild?”

The rule is simple:

Any change invalidates the current layer and everything after it.

There are no partial rebuilds.


Why builds suddenly become slow

Consider this common pattern:

COPY . .
RUN pip install -r requirements.txt

Now change a single Python file.

What happens?

  • COPY . . changes -> cache invalidated
  • RUN pip install ... runs again
  • dependency install takes time
  • build feels “slow” for no obvious reason

Nothing is wrong. Docker is doing exactly what you asked.


Layer order should follow change frequency

A useful mental shift:

Order layers by how often they change, not by how logical they feel.

Instead of this:

COPY . .
RUN pip install -r requirements.txt

Prefer this:

COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

Now:

  • code changes don’t invalidate dependency layers
  • dependency changes rebuild only what’s necessary
  • rebuilds are predictable

Fast builds are a design outcome, not a Docker option.


The hidden cache killer: build context

Docker doesn’t just look at your Dockerfile.

It also hashes the entire build context — everything you send to the daemon.

That includes:

  • source code
  • .git/
  • virtual environments
  • build artifacts
  • logs
  • anything not excluded

If your context changes, cache reuse suffers.


.dockerignore is not optional

A missing or weak .dockerignore silently breaks caching.

At minimum, you should ignore:

.git
__pycache__/
node_modules/
dist/
build/
.env

Reducing the context:

  • improves cache stability
  • reduces I/O
  • speeds up builds even when layers are reused

Cache discipline starts before the first Docker instruction runs.


Why multi-stage builds often cache better

Multi-stage builds aren’t just about smaller images.

They also separate concerns.

Example:

FROM python:3.11 AS builder
COPY requirements.txt .
RUN pip install -r requirements.txt

FROM python:3.11
COPY --from=builder /usr/local /usr/local
COPY . .

Benefits:

  • dependency layers are isolated
  • runtime image changes don’t affect build steps
  • cache reuse becomes more predictable

Smaller images are nice. Stable cache boundaries are better.


What doesn’t help as much as people think

  • random build flags
  • disabling cache globally
  • forcing --no-cache
  • rebuilding everything “just to be safe”

These hide the problem instead of fixing it.

If your Dockerfile invalidates layers constantly, no flag will save you.


A simple checklist for fast builds

When builds are slow, ask:

  • Which layer changed?
  • Was that change necessary?
  • Does this layer depend on something that changes often?
  • Is my build context larger than it should be?
  • Can this be isolated into its own stage?

Most performance wins come from answering those questions honestly ;)


The takeaway

Docker build speed isn’t magic.

It’s not about tricks. It’s not about flags.

It’s about cache discipline:

  • ordering layers by change frequency
  • controlling build context
  • creating stable cache boundaries

Once you design for the cache, Docker does the rest quietly and efficiently.