Docker Best Practices

Chapter 1 · Practice 2 of 5

Use multi-stage builds to keep build tools out of the final image

From Writing the Dockerfile

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:

  1. A first stage installs everything and builds the app.
  2. A clean final stage starts from a fresh base.
  3. 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 example AS build) so later stages can COPY --from=build by name.
  • Build a single stage with docker build --target build to 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

Dockerfile
# 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"]