Docker layer cache simulator

A Docker image is built one instruction at a time, and each result is cached. Change a file, a dependency or the order of the Dockerfile, rebuild, and see which steps are reused, which run again and why.

Last build
83 s
Cache hits
0 of 8
Builds
1
Dockerfile
Change something
Build
Build 1: First build
  1. 1 FROM node:22-alpine RUN 12 s first build · base image layers, 160 MB · key 49e3132d9814
  2. 2 WORKDIR /app RUN 0.1 s an earlier step was rebuilt · config only, no layer · key 02183150b777
  3. 3 ENV NODE_ENV=production RUN 0 s an earlier step was rebuilt · config only, no layer · key 4427113f13c5
  4. 4 COPY package.json package-lock.json ./ RUN 0.3 s an earlier step was rebuilt · copies 2 files · layer 0.4 MB · key 47bbe375ce36
  5. 5 RUN npm ci RUN 48 s an earlier step was rebuilt · layer 210 MB · key c65c75f1fa86
  6. 6 COPY . . RUN 0.6 s an earlier step was rebuilt · copies 6 files · layer 2 MB · key 4c8dee8688aa
  7. 7 RUN npm run build RUN 22 s an earlier step was rebuilt · layer 15 MB · key 540a7d4538c6
  8. 8 CMD ["node", "dist/server.js"] RUN 0 s an earlier step was rebuilt · config only, no layer · key 133a1aac105f

Durations and sizes are example values for a small Node.js service; the cache decisions follow Docker's documented rules. Keys stand in for BuildKit's SHA-256 digests.

Build context
package.json
version 1
package-lock.json
version 1
src/server.js
version 1
src/routes.js
version 1
README.md
version 1
.git/
version 1
Builds
#What changedCachedTime
1First build0/883 s

How to use the simulator

The page starts with one build already done. Make a change with the buttons, then press docker build. Each instruction shows whether it was taken from the cache or run, why, how long it took and whether it added a filesystem layer. The table on the right keeps the last ten builds.

  • Press Edit code and build, with each Dockerfile. With dependencies copied first, npm ci stays cached; with everything copied first, it runs again.
  • Press Add a dependency, build, then Undo dependency and build again. The second build is fully cached, because the cache still holds the results from before the change.
  • Press Edit README or Git commit and build. Both change the files COPY . . picks up, so everything after it runs. Turn on .dockerignore (it excludes README.md and .git), build once, and try again.

The model follows the rules in Docker's build cache documentation, and tests check them: a full rebuild with no changes is all cache hits, every step after the first miss runs, and undoing a change hits the older cache.

What are image layers?

A container image is a stack of layers plus a configuration that says what to run. Each layer is an archive of the files a build step added, changed or deleted, and is identified by a digest of its contents. When a container starts, the runtime stacks the layers with a union filesystem, so the container sees one directory tree, and adds a thin writable layer on top for anything the container changes.

Because layers are identified by their contents, identical layers are stored and transferred once. Ten images built on the same base image share its layers on disk and in the registry, and pulling an image downloads only the layers the machine does not already have.

From Dockerfile to layers

A Dockerfile is a list of instructions run in order. Only some of them change files. RUN executes a command and records the files it changed; COPY and ADD bring files in from the build context. Each of these produces a layer. Instructions such as ENV, CMD, EXPOSE and LABEL only change the image configuration; they appear in the image's history as entries the OCI specification marks as empty_layer.

Layers only add: a file deleted in a later step is hidden, not removed, from the earlier layer that contains it. That is why package caches and build tools should be cleaned up in the same RUN that created them, and why a secret copied in one step and deleted in the next is still inside the image.

How the build cache decides

For each instruction, the builder looks for a cached result made from the same starting point (the parent layer) by the same instruction. Docker's documentation sets out the rules:

  • For most instructions, comparing the instruction itself is enough.
  • For COPY and ADD, the builder also computes a checksum of the files being copied, and a changed file means a miss. A file's modification time is not part of the checksum, so touching a file does not invalidate the cache.
  • For RUN, only the command string is compared. Nothing inside the container is examined, so RUN apt-get update stays cached even when the package lists it would download have changed.
  • Once one instruction misses, every instruction after it is run again, because its parent is new.

The cache is not limited to the previous build: results from earlier builds stay until the cache is pruned, so going back to an earlier version of a file can hit again. The --no-cache option ignores the cache for one build.

Ordering instructions

Since a miss rebuilds everything below it, put instructions that change rarely near the top and those that change often near the bottom. The classic case is dependency installation, which is slow and changes rarely, against the application code, which changes on every commit. Docker's own guide recommends copying the package manager files first, installing, and then copying the rest.

The simulator's two Dockerfiles differ only in that order. After a one-line code change, "Dependencies first" rebuilds in 22.6 s and "Copy everything first" in 70.6 s, against 83 s for a build from scratch. When a dependency really does change, the slow install runs either way: 70.9 s here.

The build context and .dockerignore

The build context is the set of files sent to the builder, usually the directory the build is run from. COPY . . copies all of it, so any change to any file in it, including documentation, test data or the .git directory, invalidates that step and everything after it.

A .dockerignore file lists patterns to leave out of the context, in the same style as .gitignore. Excluded files are not sent, cannot end up in the image, and do not count toward the checksum. In the simulator, editing README.md without a .dockerignore re-runs 3 steps; with README.md ignored it re-runs 0. Ignoring node_modules, build output and secrets such as .env files is common for the same reasons.

Caching beyond one machine

The build cache normally lives on the machine that ran the build. Continuous integration runners often start empty, so every build runs from scratch unless the cache is shared. BuildKit, the default builder in current Docker, can export its cache to a registry, a local directory or a CI service's cache and import it in the next build (the --cache-to and --cache-from options).

BuildKit also adds cache mounts, such as RUN --mount=type=cache,target=/root/.npm npm ci, which keep a package manager's download cache between builds without putting it in a layer, so a changed dependency list only downloads what is new.

Smaller images

Every layer is downloaded by every machine that runs the image, so size matters for deployment speed as well as storage. The main techniques are a small base image (Alpine or distroless variants), cleaning up in the same RUN that created temporary files, and multi-stage builds: a first stage with compilers and dev dependencies builds the application, and a final stage copies in only the result, so the build tools never reach the shipped image.

Docker layer questions and answers

What is a Docker image layer?

A layer is a set of filesystem changes (files added, changed or deleted) stored as a compressed archive and identified by a digest of its contents. An image is an ordered list of layers plus a configuration. When a container runs, the layers are stacked with a union filesystem such as overlayfs, and the container gets its own writable layer on top.

Does every Dockerfile instruction create a layer?

No. RUN, COPY and ADD change the filesystem and add layers. Instructions such as ENV, CMD, EXPOSE and LABEL only change the image configuration; they appear in the image history as empty entries, which the OCI image specification marks as empty_layer. They still take part in the build cache chain.

How does the Docker build cache work?

For each instruction, the builder looks for a cached result with the same parent and the same instruction. For COPY and ADD it also compares a checksum of the files being copied; for RUN it compares only the command text. The first instruction without a match is executed, and so is every instruction after it, because their parent has changed.

Why does my npm install or pip install run on every build?

Usually because the Dockerfile copies the whole project before installing dependencies, so any source change invalidates the install step. Copy only the dependency manifests (package.json and the lock file, or requirements.txt), install, and then copy the rest. In this simulator, a one-line code change then rebuilds in 22.6 s instead of 70.6 s.

Does changing a file’s timestamp invalidate the cache?

No. According to Docker’s documentation, the modification time of a file is not part of the cache checksum for COPY and ADD, so touching a file without changing it keeps the cache. Changing its contents or other metadata invalidates it.

What is .dockerignore for?

It excludes files from the build context, the set of files sent to the builder. Excluded files cannot be copied into the image and do not affect the checksum of COPY . ., so editing them does not invalidate the cache. In this simulator, editing README.md rebuilds 3 steps without a .dockerignore and 0 with one.

Further reading

Published Updated