An image is a stack of read-only layers. RUN, COPY and ADD add filesystem layers (WORKDIR adds an empty one); the other instructions only change the image's configuration.
docker history
learner@pier:~/status-page$ docker history status-page:0.8
IMAGE CREATED CREATED BY SIZE COMMENT
2e0c9e60cd22 4 seconds ago CMD ["node","server.js"] 0B
<missing> 4 seconds ago EXPOSE 3000 0B
<missing> 4 seconds ago RUN /bin/sh -c npm install # buildkit 3.9MB buildkit.dockerfile.v0
<missing> 4 seconds ago COPY . . # buildkit 1.54kB buildkit.dockerfile.v0
<missing> 4 seconds ago WORKDIR /app 0B
<missing> 12 days ago CMD ["node"] 0B
<missing> 12 days ago ENTRYPOINT ["docker-entrypoint.sh"] 0B
...
<missing> 12 days ago ADD alpine-minirootfs-3.20.3-x86_64.tar.gz /… 74.3MB buildkit.dockerfile.v0Newest first. Rows from 12 days ago are the base image's layers. Images built from the same base share those layers on disk.
How the cache decides
For each step, BuildKit reuses the stored result (CACHED) when:
- the previous step was reused, and
- the instruction text is identical, and
- for
COPY/ADD, the copied files' contents are unchanged.
The first step that misses runs, and every step after it runs too.
Order decides what reruns
COPY . . before npm install COPY package.json first
───────────────────────────── ──────────────────────────────
FROM node:20-alpine FROM node:20-alpine
WORKDIR /app CACHED WORKDIR /app CACHED
COPY . . runs ◀ server.js COPY package.json ./ CACHED
RUN npm install runs RUN npm install CACHED
COPY . . runs ◀ server.jsAfter editing server.js with the left-hand Dockerfile:
=> CACHED [2/4] WORKDIR /app 0.0s
=> [3/4] COPY . . 0.1s
=> [4/4] RUN npm install 0.5sWith the right-hand Dockerfile, after the same kind of edit:
=> CACHED [2/5] WORKDIR /app 0.0s
=> CACHED [3/5] COPY package.json ./ 0.0s
=> CACHED [4/5] RUN npm install 0.0s
=> [5/5] COPY . . 0.1snpm install now reruns only when package.json changes. In a real project that is the difference between seconds and minutes per build. If the project has a package-lock.json, copy it with package.json (COPY package*.json ./).
Rules of thumb
| Do | Why |
|---|---|
| Put rarely changing steps first (base, OS packages, dependencies) | They stay cached |
| Copy dependency manifests before the code | Code edits do not reinstall |
Keep apt-get update and install in one RUN | They are cached and invalidated together |
Use docker build --no-cache when you need fresh packages | RUN is cached by text only |