Layers and the build cache

Docker · Practical · 12 min

A well-ordered Dockerfile rebuilds in seconds instead of minutes, all day long.

Each instruction that changes the filesystem (RUN, COPY, ADD) produces a layer: a diff of files added, changed or deleted. The image is the stack of these layers. Metadata instructions like ENV and CMD only change the image config.

When you rebuild, the builder checks each step against its cache. A step is reused if its instruction is unchanged and, for COPY/ADD, the checksums of the copied files are unchanged. The crucial rule: once a step misses the cache, every step after it is rebuilt.

So order instructions from least to most frequently changing. The standard pattern copies the dependency manifest first, installs, and only then copies the source:

COPY package.json package-lock.json ./
RUN npm ci
COPY . .

Editing source code now invalidates only the last COPY; the slow npm ci stays cached until the lockfile changes. Copying everything first would reinstall dependencies on every code change.

Other layer facts that matter:

  • Deleting a file in a later layer does not shrink the image; the earlier layer still contains it. Clean up in the same RUN that created the mess: apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*.
  • RUN apt-get update alone in its own step gets cached forever, leading to stale package lists; always chain it with the install.
  • The cache key is the instruction text, not the outside world, so RUN curl .../latest.tar.gz won't refetch. Use --no-cache or a changing build arg when you need fresh content.

docker image history <image> shows each layer's size and the instruction that made it.

Example

# syntax=docker/dockerfile:1
FROM node:22-slim
WORKDIR /app

# 1. Rarely changes: cached unless the lockfile changes
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# 2. Changes on every commit: only this layer and below rebuild
COPY . .

# Install + clean up in ONE layer, or the cleanup saves nothing
RUN apt-get update && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

CMD ["node", "server.js"]

Remember

  • Each RUN/COPY/ADD creates a layer; a cache miss rebuilds everything after it
  • Copy dependency manifests and install before copying source
  • Clean up in the same RUN; later deletions don't shrink earlier layers
  • Chain apt-get update with install in one RUN

Watch out for

  • COPY . . near the top of the Dockerfile, so every code edit reinstalls all dependencies.

Check yourself

  1. You change one line in src/app.js. Which steps rebuild in the example Dockerfile?
  2. A RUN step downloads a 300 MB archive and a later RUN deletes it. How much does the deletion save in the final image?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.docker.com/build/cache/