Open a random production Dockerfile and you will usually find the same three sins: copying the whole repository first, installing dependencies after the source, and shipping the build toolchain to production.
Each one is small. Together they turn a 90 MB image into 1.4 GB and a 40-second rebuild into a nine-minute one.
Layers are a cache, and order is everything
Docker caches each layer keyed on the instruction and the files it touches. Change anything above a layer and every layer below it is rebuilt. So put the things that change least at the top.
# Bad: any source edit re-installs every dependency
COPY . /app
RUN npm ci
# Good: dependencies rebuild only when the lockfile changes
COPY package.json package-lock.json ./
RUN npm ci
COPY . /app
That single reorder is usually the difference between a cache hit and a full install on every commit. The Docker docs cover the layer model in more depth than most tutorials bother to.
Multi-stage builds: stop shipping your compiler
The build stage needs the SDK, the compiler and every dev dependency. The runtime needs almost none of it. Multi-stage builds let you throw the first stage away:
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
The final image now contains the compiled output and production dependencies — nothing else. On a typical TypeScript service that is the difference between 1.1 GB and about 180 MB.
A checklist that pays for itself
- Start from a specific tag, never
latest. - Copy lockfiles before source.
- Use
npm ci, notnpm install, so the lockfile is authoritative. - Drop dev dependencies with a production install or a second stage.
- Set
NODE_ENV=productionin the runtime stage only. - Run as a non-root user; it costs one line.
- Put a
.dockerignorenext to the Dockerfile or you will copy.gitandnode_modulesanyway.
None of this is clever. It is just ordering, and ordering is free.
Conversation
1 comment