Lesson 02/12 minutes/2 graded
Images, layers, and the cache
Why your rebuild takes four seconds or four minutes, and what decides which.
An image is a stack of read-only layers plus a small amount of metadata. Each instruction in a Dockerfile that changes the filesystem produces one layer. When you run the image, Docker adds a thin writable layer on top and hands the result to the process as its root filesystem.
That layering is what makes the cache work, and what makes it miss.
Layers are content-addressed, so they are cached
Each layer is identified by a hash of its contents and of every layer beneath it. When you rebuild, Docker walks the Dockerfile and, for each instruction, asks: have I built this exact instruction on top of this exact parent before? If yes, it reuses the cached layer and moves on.
When one instruction misses, everything below it misses too. The parent hash changed, so nothing downstream can match what was cached before.
Which means instruction order is a performance decision
Here is the version almost everyone writes first:
FROM node:24-slim
WORKDIR /app
COPY . .
RUN npm ci
CMD ["node", "server.js"]
Edit one line of source, rebuild, and npm ci runs again from scratch. COPY . .
included your source, your source changed, so that layer missed, and RUN npm ci
sits below it.
Now the same build with the dependency step lifted above the source:
FROM node:24-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]
Editing source now only invalidates the final COPY. The install layer is
reused, because package.json and the lockfile did not change.
On the course sample project, editing one source file and rebuilding:
| Ordering | Rebuild |
|---|---|
| Dependencies above source | 8.3 s |
COPY . . above npm ci | 16.0 s |
The saving is exactly the cost of your dependency install. The sample has four packages, so the gap is seconds. On a project with a real dependency tree the same mistake costs minutes, and the ordering therefore matters more the bigger the project gets.
Multi-stage builds, briefly
A build stage can copy artefacts out of an earlier stage and discard the rest. The compiler, the dev dependencies, the source tree: none of it needs to ship.
FROM node:24-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:24-slim AS prod-deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
FROM node:24-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=prod-deps /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
The final image contains the last stage only. The compiler and the source tree are gone,
and because dependencies come from prod-deps rather than build, the dev
dependencies stay behind as well. Copying node_modules out of the build stage
is the common mistake here: plain npm ci installs devDependencies, and they
ship with the image unless a separate stage installs without them.
Tagging
An image with no tag gets latest, which is not a version. It is a default
label that moves. Name images deliberately: