Chapter 1 · Practice 2 of 5
Use multi-stage builds to keep build tools out of the final image
Why it matters
Compilers, dev dependencies and test tools are needed to build your app, not to run it. Shipping them makes the image bigger and hands an attacker extra tools.
A multi-stage build fixes this:
- A first stage installs everything and builds the app.
- A clean final stage starts from a fresh base.
- The final stage copies only the built output from the first stage with
COPY --from.
The image you ship holds what the app needs to run and nothing else.
- Name stages with
AS(for exampleAS build) so later stages canCOPY --from=buildby name. - Build a single stage with
docker build --target buildto run tests in CI.
After the change, compare sizes with docker image ls. A Node or Go service often drops by more than half.
Example
# syntax=docker/dockerfile:1
FROM node:24-slim AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev
FROM node:24-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]