Build images
Use layer caching deliberately
Order Dockerfile instructions so dependency installation is reused until its real inputs change.
Docker caches every step of a build. When you rebuild, it checks each instruction: if the instruction is the same and its inputs haven’t changed, it reuses the layer from last time and prints CACHED. The first step that changes gets rebuilt, and so does every step after it, no matter what.
That last rule is the one to design around. Order matters.
The slow version
Look at this Dockerfile:
FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci --omit=dev
CMD ["node", "server.js"]
Every time you edit server.js, the COPY . . layer changes. So npm ci runs again and downloads every dependency, even though package.json didn’t move. On a real project that’s a minute wasted per build.
The fast version
Copy the dependency manifests first, install, then copy the code:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY server.js ./
CMD ["node", "server.js"]
Now editing server.js only invalidates the last COPY. The npm ci layer stays cached. Changing package.json or package-lock.json still rebuilds it, which is what we want.
package*.json matches both package.json and package-lock.json. npm ci needs the lockfile and fails without one, so run npm install once locally to create it. --omit=dev skips devDependencies, which the running server doesn’t need.
Correct first, fast second
A cached layer is only correct if Docker saw all its real inputs. If your install depends on an .npmrc file, or on a script in a separate file, copy those before the RUN npm ci too. Otherwise you can change them and Docker will happily reuse a stale layer, because from its point of view nothing changed.
Two flags people misuse
--no-cache forces every step to run again. Use it when you suspect a stale layer, not as your daily build command. And note that it does not re-download the base image. If you want a fresh node:22-alpine, add --pull:
docker build --pull --no-cache -t notes-api:dev .
BuildKit also has cache mounts, which keep the npm download cache between builds. They’re a nice speedup, but treat them as disposable. Your build must still work when the cache is empty, because on a fresh CI runner it will be.
Try this on the Notes API: add one dependency with npm install, build twice, and see CACHED on the install step the second time. Then edit only server.js and build again. The install step stays CACHED, the last COPY doesn’t.
Lesson completed