The Build Cache

The Docker build cache is Docker’s shortcut for rebuilding images quickly. Instead of repeating every Dockerfile step, Docker reuses earlier results when an instruction and its inputs have not changed.

This matters because real images often install packages, download dependencies, compile assets, and copy source code. Good cache behavior can turn a rebuild from minutes into seconds, while one poorly ordered COPY line can make every edit feel slow.

Overview: How The Build Cache Works

A Docker image is a stack of read-only layers plus image metadata. During docker build, Docker reads the Dockerfile from top to bottom. Instructions such as RUN and COPY create filesystem changes; metadata instructions such as CMD, ENV, EXPOSE, and USER update the image configuration. When you later run the image, Docker adds a thin writable container layer on top of those read-only image layers.

The build cache is tied to that sequence. For each instruction, BuildKit compares the current instruction with previous builds. The cache key includes the instruction text, the image state produced by the previous instruction, and, for COPY and ADD, the files used from the build context. If Docker can prove that the result would be the same, it reuses the cached layer instead of running the instruction again.

Cache invalidation is the most important rule: when one instruction cannot use cache, that instruction and every later dependent instruction must be rebuilt. Docker cannot safely reuse a layer that was built on top of a different parent state. This is why Dockerfile order matters so much. Put slow, stable work early; put fast-changing files late.

For example, a Node.js project should usually copy package.json and package-lock.json, run npm ci, and only then copy the rest of the source. If you copy the whole project before installing dependencies, changing one line in server.js changes the input to the earlier COPY, which forces the dependency install to run again.

The build context also affects caching. The context is the directory, URL, or standard input sent to the builder. COPY . . does not mean your whole computer; it means the files in the build context after .dockerignore is applied. A noisy context with node_modules, logs, test output, and Git data is slower to send and more likely to invalidate cache unexpectedly.

Syntax

The main build command is:

docker build [OPTIONS] PATH | URL | -
Option or instruction Cache meaning
PATH The build context. File checksums from this context affect COPY and ADD cache keys.
-f, --file Selects the Dockerfile. Changing the Dockerfile text can invalidate affected steps.
-t, --tag Names the final image. Tagging does not change cache behavior by itself.
--no-cache Ignores cached results and rebuilds each instruction.
--pull Checks for newer base image versions. A changed base image invalidates later layers.
COPY and ADD Use the contents of copied files as cache inputs.
RUN Can be reused when the command text and prior image state match. Docker does not inspect whether a package mirror would return newer packages.
RUN --mount=type=cache BuildKit feature that keeps a reusable directory for package manager downloads without baking that cache into the final layer.

Examples

Example 1: A cache-friendly Node.js Dockerfile

This Dockerfile uses a pinned base image tag, copies dependency manifests first, installs production dependencies, then copies the frequently changing app source:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY server.js ./
USER node
EXPOSE 3000
CMD ["node", "server.js"]

Build it:

docker build -t cache-demo-api:1.0 .

Output:

[+] Building 12.4s (9/9) FINISHED
 => [internal] load build definition from Dockerfile
 => [internal] load .dockerignore
 => [internal] load metadata for docker.io/library/node:20-alpine
 => [1/5] FROM docker.io/library/node:20-alpine
 => [2/5] WORKDIR /app
 => [3/5] COPY package*.json ./
 => [4/5] RUN npm ci --omit=dev
 => [5/5] COPY server.js ./
 => naming to docker.io/library/cache-demo-api:1.0

The first build has little or no local cache, so Docker runs the install step. EXPOSE 3000 records metadata only; it does not publish a host port. Running the image later would still need docker run -p 3000:3000 cache-demo-api:1.0 if you want host access.

Example 2: Rebuild after only source code changes

If you edit server.js and rebuild with the same command, the dependency layers can be reused:

docker build -t cache-demo-api:1.1 .

Output:

[+] Building 1.2s (9/9) FINISHED
 => CACHED [2/5] WORKDIR /app
 => CACHED [3/5] COPY package*.json ./
 => CACHED [4/5] RUN npm ci --omit=dev
 => [5/5] COPY server.js ./
 => naming to docker.io/library/cache-demo-api:1.1

The tag changed from 1.0 to 1.1, but the tag is not what controls the cache. Docker reused the WORKDIR, manifest copy, and npm ci result because their inputs were unchanged. Only the source copy and final image metadata needed new work.

Example 3: Use BuildKit cache mounts for downloads

A cached layer and a cached package download directory solve different problems. The layer cache skips an instruction entirely. A BuildKit cache mount helps when an instruction must run again but can reuse downloaded files:

# syntax=docker/dockerfile:1.7
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
COPY server.js ./
USER node
CMD ["node", "server.js"]

Build it as usual:

docker build -t npm-cache-demo:1.0 .

Output:

[+] Building 7.8s (9/9) FINISHED
 => [internal] load build definition from Dockerfile
 => [internal] load metadata for docker.io/library/node:20-alpine
 => [3/5] COPY package*.json ./
 => [4/5] RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev
 => [5/5] COPY server.js ./
 => naming to docker.io/library/npm-cache-demo:1.0

If package-lock.json changes, npm ci must run again because the dependency inputs changed. The cache mount can still make that run faster by preserving npm’s download cache between builds, without adding /root/.npm to the image layer.

Example 4: Force a clean rebuild

Sometimes you deliberately want to ignore the cache, such as when checking whether a Dockerfile still builds from scratch:

docker build --no-cache -t cache-demo-api:clean .

Output:

[+] Building 13.1s (9/9) FINISHED
 => [internal] load build definition from Dockerfile
 => [internal] load .dockerignore
 => [2/5] WORKDIR /app
 => [3/5] COPY package*.json ./
 => [4/5] RUN npm ci --omit=dev
 => [5/5] COPY server.js ./
 => naming to docker.io/library/cache-demo-api:clean

--no-cache is useful for verification, but it should not be your normal fix for confusing builds. If you need it constantly, the Dockerfile may depend on moving external state, missing lockfiles, or unclear build inputs.

How It Works Step By Step

  1. The Docker CLI sends the Dockerfile, chosen options, and build context to the builder. Files ignored by .dockerignore are removed before they become cache inputs.
  2. The builder resolves the FROM image. If the selected base image differs from the one used before, every later step has a new parent state and must be rebuilt.
  3. For each instruction, BuildKit computes whether the previous result can be reused. For RUN, the command text and previous image state are central. For COPY, the copied file contents are also central.
  4. On a cache hit, Docker reports CACHED and links the existing result into the new build graph.
  5. On a cache miss, Docker runs the instruction. A RUN command executes in a temporary build container, then Docker records the resulting filesystem difference as a new read-only layer.
  6. After the first miss, later instructions are considered against the new state. Even unchanged text may rebuild because the parent layer changed.
  7. At the end, Docker writes the image configuration and updates the requested tag. Existing containers do not change just because an image tag was rebuilt.

Common Mistakes

Copying all source before installing dependencies

This builds, but it is slow during development:

FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci --omit=dev
CMD ["node", "server.js"]

A tiny edit to any copied file changes the COPY . . cache key, so npm ci runs again. Fix it by copying dependency manifests first:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]

Forgetting .dockerignore

Without .dockerignore, Docker may send dependency folders, logs, test output, and local secrets as part of the build context. A practical Node.js project often starts with:

node_modules
.git
*.log
coverage
dist
.env

This improves speed and reduces accidental exposure. It is not a substitute for secret management, but it prevents many avoidable context problems.

Assuming RUN always checks the internet again

This instruction can be cached even if the Alpine package repository has changed since yesterday:

RUN apk add --no-cache curl

Docker does not know that an external package index changed. It sees the same instruction on the same parent image and can reuse the old result. Use --no-cache for a deliberate clean rebuild, and use --pull when you intentionally want Docker to check for a newer tagged base image.

Baking secrets into layers

This is unsafe with real credentials:

FROM alpine:3.20
ARG API_TOKEN
RUN echo "$API_TOKEN" > /tmp/token.txt
RUN rm /tmp/token.txt

Removing a file in a later layer does not erase it from an earlier layer. Secrets should come from BuildKit secret mounts, Docker secrets, mounted files, CI secret stores, or your orchestrator, not ordinary ARG, ENV, COPY, or RUN echo.

Best Practices

  • Order Dockerfile instructions from least-changing to most-changing.
  • Copy lockfiles and dependency manifests before copying application source.
  • Keep a small, intentional .dockerignore in every real project.
  • Use pinned base image tags such as node:20-alpine, not latest, so cache and rebuild behavior are easier to reason about.
  • Use lockfiles, such as package-lock.json, so dependency installs are repeatable.
  • Use BuildKit cache mounts for package manager download caches when an install step must run often.
  • Use --no-cache for diagnosis and clean-build verification, not as a normal development habit.
  • Use --pull intentionally in CI if you want regular base-image refreshes.
  • Never put secrets into image layers. A later delete does not remove data from image history.
  • Remember that rebuilding an image does not update already-running containers; recreate containers to use the new image.

Practice Exercises

  1. Take a Node.js Dockerfile that starts with COPY . .. Reorder it so npm ci stays cached when only server.js changes. Hint: copy package files first.
  2. Create a .dockerignore for a project that has node_modules, .git, logs, test coverage, and a local .env file. The expected end state is a smaller build context.
  3. Build the same image twice, then change only one source file and build again. Read the output and identify which steps say CACHED and which steps rerun.

Summary

  • The build cache reuses earlier image build results when instructions and inputs have not changed.
  • A cache miss invalidates that instruction and later dependent instructions.
  • COPY and ADD depend on files from the build context, so .dockerignore matters.
  • Cache-friendly Dockerfiles install dependencies before copying fast-changing source code.
  • BuildKit cache mounts can speed up repeated package installs without storing package caches in the final image.
  • --no-cache forces a clean rebuild; --pull checks for newer base images.
  • Secrets must not be baked into image layers, because later deletion does not erase earlier layer contents.