Docker Layer Caching: An In-Depth, No-BS Guide

I remember the exact moment I fell in love with Docker. It was 2018, and we were wrestling a deployment that took 45 agonizing minutes. Pushing a single line...

docker layer caching in-depth no-bs guide
By Nishaant Dixit
Docker Layer Caching: An In-Depth, No-BS Guide

Docker Layer Caching: An In-Depth, No-BS Guide

Free Technical Audit

Expert Review

Get Started →
Docker Layer Caching: An In-Depth, No-BS Guide

I remember the exact moment I fell in love with Docker. It was 2018, and we were wrestling a deployment that took 45 agonizing minutes. Pushing a single line of Python code meant waiting almost an hour to see it in production. It was pure misery.

Then we switched to Docker. Our builds went from 45 minutes to 90 seconds. But here's the punchline — the first time I ran a build, I saw something odd. The second build was instant. No output. Just a message: Using cache.

That was my introduction to docker layer caching. And I had absolutely no idea what it meant. I just knew it was fast.

Five years later, I've built production systems at SIVARO that process 200,000 events per second. I've watched engineers waste days fighting caching bugs. I've seen teams destroy their deployment speed with one poorly placed COPY command. This guide is everything I wish someone had told me in that first week.

What Docker Layer Caching Actually Is (And Isn't)

Most people think Docker is a virtual machine. It's not. Docker is a process isolator. It shares your host kernel, isolates the filesystem, and packages everything into what we call an image.

Here's the definition you'll find on GeeksforGeeks: "Docker is a set of platform-as-a-service products that use OS-level virtualization to deliver software in packages called containers." Technically correct. Completely useless for understanding why caching works.

Here's what actually matters.

When you build a Docker image, you're not building a single thing. You're building a layer cake. Every instruction in your Dockerfile creates a new layer. FROM creates layer one. RUN apt-get install creates layer two. COPY . . creates layer three. Each layer is a diff — just the changes from the previous layer.

Docker stores these layers in a content-addressable store. That means each layer gets a unique hash based on its content. When Docker sees a layer it's already built, it says: "Hey, I've seen this exact hash before. I'll reuse it."

This is docker layer caching. The build system checks if a layer exists. If it does, it skips the execution and just references the cached layer.

The magic is that this applies to every layer. Not just the base image. If my npm install layer is cached, I can rebuild my app in 5 seconds instead of 5 minutes. This isn't just a nice-to-have. This is the difference between scaling ten times a day and scaling once a week.

The Read-Heavy Nature of Docker Layers

Let's talk about what Docker does when it starts a container. It doesn't copy all the files. It uses a union filesystem — typically OverlayFS or, in newer setups, the snapshotter that comes with containerd vs. Docker discussions.

Here's the critical insight. When a container needs to read a file, it looks through each layer from top to bottom. The first match wins. When a container writes a file, it copies the file to the topmost layer (the writable container layer) and modifies it there. This is called copy-on-write.

The performance implication is massive. Read-heavy workloads are fast. The file lives in the page cache. But if a container writes to a file that's deep in the layer stack? Every container that starts from that base layer needs to handle that copy-on-write overhead.

Most people don't think about this. They just build images and push them. But if you're running thousands of containers from the same image, the read performance of your lower layers determines your cold start time.

At SIVARO, we tested this in production. We had a Rails app that took 45 seconds to boot. We reorganized the Dockerfile so the gems layer was at the bottom. Boot time dropped to 22 seconds. No other changes. Just layer ordering.

The Build Context: Where Caching Dies

There's a silent killer in every Docker build. The build context.

When you run docker build ., Docker packs up the entire directory — your source code, node_modules, the .git folder, that random .env file you forgot — and sends it to the Docker daemon. This happens before any instruction executes. And the size of this context affects your build time even if the cache is warm.

Here's the problem. If you have a COPY . . in your Dockerfile, Docker computes the hash of every file in the context. If ANY file changed — even a single character in a comment — the cache for that layer and every layer after it is invalidated.

Let me show you what this means in practice.

dockerfile
# Bad - this will invalidate the cache every time ANY file changes
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install --production
RUN npm run build

Every single time you change a source file, COPY . . sees a different hash, invalidates the cache, and you're re-running npm install from scratch. On a project with 2,000 dependencies, that's an extra 3-4 minutes of build time for every change.

Here's the fix:

dockerfile
# Good - package.json changes rarely, source code changes often
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install --production
COPY . .
RUN npm run build

Now npm install only runs when package.json or package-lock.json changes. The COPY . . will still invalidate downstream layers when source files change, but those layers are fast — just a copy and a build.

This is the single most important thing I teach new engineers. Docker layer caching how does it work in practice? It works like this. Order your Dockerfile from least-changing instructions to most-changing instructions. Put your dependencies first. Put your source code last.

A Caveat About False Cache Hits

Now I'm going to tell you something that most tutorials skip.

Docker's cache validation isn't perfect. It compares the hash of the instruction and the files, but it doesn't look at the content of files if they're not referenced by the instruction. And for COPY and ADD, it only checks the file metadata.

Wait, is that right? Let me be more precise.

Docker checks the checksum of the file contents for COPY and ADD. It changed behavior across versions. But here's the thing that actually trips people up. Docker doesn't invalidate the cache based on what a RUN command does. It only looks at the command string itself.

dockerfile
RUN apt-get install python3

If I change this to:

dockerfile
RUN apt-get install python3 && apt-get install curl

Docker sees a different command string and rebuilds. Good. But here's the catch:

dockerfile
RUN curl https://example.com/latest-version.tar.gz > /tmp/latest.tar.gz

If that URL returns different content every time, Docker doesn't know. It sees the same command string, says "cache hit," and uses the old layer. You get stale data. This is a false cache hit.

Docker has a line of defense — the --no-cache flag — but that's sledgehammer solution.

bash
docker build --no-cache -t myapp:latest .

The surgical fix is to use BuildKit's cache mount feature.

dockerfile
# syntax=docker/dockerfile:1
FROM golang:1.22-alpine
WORKDIR /app
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/root/.cache/go-build     go build -o /app/main .

The --mount=type=cache points Docker at a persistent cache that survives layer invalidation. This is a game-changer for dependency downloads. That go build cache won't be thrown away just because a source file changed. I've seen build times drop from 8 minutes to 40 seconds with this single line.

Docker Swarm vs Kubernetes: Which Is Easier?

Before I go deeper, let me briefly address the elephant in the room. If you're building with Docker, you're probably thinking about orchestration. And the question I get more than any other is "docker swarm vs kubernetes which is easier."

The answer, from someone who's run both in production: Docker Swarm is easier by an order of magnitude.

Swarm is built into Docker. You run docker swarm init, add a few nodes, and you're done. There's no separate control plane to manage, no etcd to babysit, no admission controllers to configure. It just works.

Kubernetes is a monster. It gives you incredible power — autoscaling, self-healing, service mesh integration — but you pay for it with complexity. You need to understand Pods, Deployments, Services, Ingresses, ConfigMaps, Secrets, and half a dozen other abstractions just to deploy a hello world.

But here's the twist. Kubernetes won. Not because it's better, but because the ecosystem is heavier. K3s, minikube, EKS, GKE, AKS — there are a dozen ways to run it. Swarm is feature-complete but stagnant. The Docker interview questions all mention Kubernetes as the standard. Swarm barely registers.

My recommendation: if you're a small team building a single app, use Swarm. If you're planning to scale, use Kubernetes. There's no middle ground, and pretending otherwise costs you time later.

Remote Caching with BuildKit and Buildx

Now let's talk about the part that actually scales. Local layer caching works great for single developers. But in a CI pipeline, you have a problem: every build agent starts with a cold cache. The first build of the morning takes 15 minutes. Every build after that takes 30 seconds.

The world's most awesomest solution to this is remote caching. BuildKit lets you export your cache to a registry or an external store. This means your CI agents share a cache across builds.

bash
docker buildx build   --cache-from=type=registry,ref=myregistry/cache:latest   --cache-to=type=registry,ref=myregistry/cache:latest,mode=max   -t myapp:latest   .

This exports the cache to myregistry/cache:latest. The next build can pull that cache and rebuild in seconds. The tricky part is that the --cache-to flag defaults to mode=min, which only caches layers that are actively used. If your build creates an intermediate layer that's discarded, it won't be cached. Use mode=max for full caching.

We run this at SIVARO across our GitHub Actions runners. Our npm install layer is cached globally. Every PR build takes 2 minutes instead of 8. That's a 75% reduction in CI time, and it costs about $25 a month for the cache storage.

Docker vs Containerd for Production Workloads

Docker vs Containerd for Production Workloads

I can't talk about image caching without mentioning the underlying architecture.

For years, Docker was the only game in town. But Docker itself is just a client. Underneath it, you have the daemon (dockerd), which uses a runtime to actually run containers. The default runtime is containerd — a lightweight container runtime that manages images and lifecycle.

The question I see everywhere is "docker vs containerd for production workloads." Here's the straight answer.

Containerd is faster and more stable. It's a stripped-down runtime with fewer moving parts. It talks directly to the kernel, has no daemon to handle Docker's REST API, and boots containers in milliseconds. Docker is a full platform. It includes the daemon, the CLI, swarm mode, volumes, networking, and the whole Docker Compose stack.

Benchmarks consistently show containerd uses less memory and has lower startup latency. But Docker is easier for humans. docker ps, docker compose up, docker logs — these are friendly commands. containerd uses ctr, which is 17 times less intuitive.

Which should you use? If you're running Kubernetes, the answer is irrelevant. Kubernetes uses containerd by default. If you're running standalone, Docker for development and containerd for production is the magic combination.

Simplest way to think about this — Docker is the camera with all the features. Containerd is the lens. If you're doing professional work, you need the lens.

The Specifics of Cache Invocation

Now let's get back to docker layer caching how does it work on a granular level.

When Docker executes a build, it goes through each instruction. For each one, it calculates a cache key. For instructions like RUN, the cache key is the command string plus the hash of the parent layer. For COPY and ADD, it's the file checksums plus the parent hash.

If the cache key matches an existing layer, Docker uses it. If it doesn't, Docker executes the instruction, creates a new layer, and stores it.

Here's what trips up people. Changing an instruction in the middle of a Dockerfile only invalidates that layer and everything after it. Layers before it are untouched.

dockerfile
FROM node:20-alpine           # Layer 1 (cached)
RUN npm install -g typescript # Layer 2 (cached)
COPY package.json ./          # Layer 3 (cached)
COPY . .                      # Layer 4 (INVALIDATED)
RUN npm run build             # Layer 5 (rebuild)

The COPY . . invalidates layers 4 and 5, but layers 1-3 come from cache. This is why dependency installation stays fast even on the 50th rebuild. The base image and global packages never change.

Let me show you a real Dockerfile pattern that takes full advantage of this.

dockerfile
# syntax=docker/dockerfile:1
# First, pin your base image. Never use :latest.
FROM python:3.12-slim AS base

# Environment variables first. They rarely change.
ENV PYTHONDONTWRITEBYTECODE=1     PYTHONUNBUFFERED=1

# Dependencies next. The firewall is above, the source is below.
WORKDIR /app
COPY pyproject.toml poetry.lock ./
RUN pip install --no-cache-dir --upgrade pip &&     pip install --no-cache-dir .

# Non-root user for security.
RUN useradd --create-home --shell /bin/bash appuser
USER appuser

# Source code last. This is the layer that changes most.
COPY --chown=appuser:appuser . .

# Application entrypoint.
CMD ["uvicorn", "myapp.main:app", "--host", "0.0.0.0", "--port", "8000"]

Notice what's happening. The source code is copied in at the end, after all the expensive operations. The pip install layer gets cached on the first build and never runs again — even when you change every white pixel of your UI code. This is exactly how to think about layer caching.

Common Mistake: Cache-Destroying Dockerfiles

I've seen the same mistake in countless production codebases. A Dockerfile that looks like this:

dockerfile
FROM ubuntu:latest
COPY . /app
RUN apt-get update && apt-get install -y python3
RUN cd /app && pip install -r requirements.txt

The COPY . /app at the top makes the entire build cache-hostile. Every file change forces Docker to re-run the install commands. Fix the order — COPY dependencies first, then source.

I get this question often as part of Docker interview questions and answers all level. The interviewer will show you a badly ordered Dockerfile and ask you to fix it. They're testing whether you understand the principle of stability-first ordering.

Practical Performance Numbers

Let me tell you what this means in numbers.

We build a typical Python microservice at SIVARO. Base image: python:3.12-slim. Dependencies: ~120 packages. Source code: ~2,000 files.

  • First build (cold cache): 3 minutes 20 seconds
  • Second build (no source changes): 2 seconds
  • Build with one source file changed: 11 seconds
  • Build with pyproject.toml changed: 2 minutes 15 seconds

To compare, here's the same microservice with a badly ordered Dockerfile (source code copied first):

  • First build (cold cache): 3 minutes 20 seconds
  • Second build (no source changes): 2 seconds
  • Build with one source file changed: 3 minutes 12 seconds

That's an 18x difference in iteration time. When you're debugging a production incident, 3 minutes versus 11 seconds for a rebuild is the difference between fixing the bug before coffee cools and sending the entire afternoon down the drain.

Docker Compose and Caching

One thing I see teams mess up is caching with Docker Compose. Compose builds images in parallel by default, but it uses the same Docker daemon and the same layer cache. The optimization is to build images with cache and then start containers with --no-recreate to avoid re-creating unchanged containers.

yaml
version: '3.9'
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile.api
    image: myapp-api:latest

  worker:
    build:
      context: .
      dockerfile: Dockerfile.worker
    image: myapp-worker:latest

When I run docker compose build, Docker builds both images using the shared cache. Layers that are common between the two Dockerfiles — base image, common utilities — are reused across images. This is one of the hidden advantages of Docker's design. It's not just caching within an image. It's caching across images.

FAQs: Docker Layer Caching

What does "using cache" mean in docker build?
It means Docker found a layer with the same cache key as the instruction it's trying to execute. It skips re-running the instruction and uses the stored layer. Your build is faster because expensive operations like npm install or apt-get don't re-run.

How do I force Docker to rebuild a layer?
Use docker build --no-cache -t myapp . to disable caching entirely. Use docker build target stage — no-cache if you're using multi-stage builds. If you want to target a single layer, change the instruction — Docker matches on the exact command string and parent hash.

Why is my cache not being used?
Most likely because a previous COPY or ADD instruction had modified files. Docker invalidates all subsequent layers when any input file changes. Another possibility: you're using the --no-cache flag or your Docker daemon is running out of disk and purging the cache.

Does Docker cache across different Dockerfiles?
Yes, within the same daemon and if they share base layers. If two Dockerfiles both start with FROM python:3.12-slim, the base image layer is shared. If they run the same apt-get install, the layer might be shared if the instruction and parent are identical.

Does changing an environment variable invalidate the cache?
Yes, ENV instructions are part of the cache key. Changing ENV PYTHONPATH=/src will invalidate every layer after it. Keep ENV at the top of the Dockerfile so the invalidation zone is as small as possible.

Does docker build use cache from a remote registry?
By default, no. It only uses local cache. You need to set up registry sidecache with BuildKit (--cache-from=type=registry,ref=...) to share caches across machines.

The Bottom Line on Docker Layer Caching

The Bottom Line on Docker Layer Caching

Docker layer caching sounds complicated, but it's actually just an ordering problem. Put the things that change least at the bottom of your Dockerfile. Put the things that change most at the top. That's the whole rule.

At SIVARO, I've seen this change our deployment culture. Builds that took 10 minutes now take 30 seconds. Our developers iterate faster, our CI is cheaper, and our production incidents are shorter.

The rest is just specifics. If you want to go deeper, check the Top Docker Interview Questions and Answers for the common traps, and read the containerd docs for the nitty-gritty of layer storage backends.

You'll find that the fundamentals are simple. The hard part is remembering them when you're under pressure. But that's what experience is for. Let's build.


Nishaant Dixit — Founder of SIVARO. Building data infrastructure and production AI systems since 2018. Built systems processing 200K events/sec.

Part of our Docker series — see every guide in this cluster. Fighting this in production? Explore MVP to Production.

Free · No Commitment · 48-Hour Delivery

Get a free infrastructure audit

2-hour remote session. We audit your data infrastructure, identify what's costing you time and money, and deliver a written roadmap with specific, measurable targets. No pitch.

Book Your Free Audit
N
Nishaant Dixit
Founder & Lead Engineer at SIVARO

Building data-intensive systems since 2018. 200K events/sec pipelines, production RAG systems, Kubernetes infrastructure. LinkedIn →

Start a Project
Need help with infrastructure?

Kubernetes, Karpenter, DevOps pipelines, and container orchestration for production workloads.

Explore MVP to Production