shouldenough

DevOpsDocker for Developers

Lesson 2 of 6

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:

OrderingRebuild
Dependencies above source8.3 s
COPY . . above npm ci16.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:

~/projects/api

Build the image in the current directory and tag it shouldenough/api version 1.0.

Check yourself

Knowledge check2 questions
Question 1 of 2
Why does `COPY . .` before `RUN npm ci` make rebuilds slow?