almessadi.
Back to Index

Use Multi-Stage Docker Builds to Ship Less_

Multi-stage builds reduce image size and attack surface, but the real goal is not Alpine by itself. The goal is shipping only what production needs.

PublishedJune 8, 2024
Reading Time4 min read

The best reason to use multi-stage Docker builds is not aesthetics. It is discipline.

Production images should contain the application and the runtime it needs to execute. They should not also contain:

  • compilers
  • package manager caches
  • test tooling
  • source files that are only useful at build time

Multi-stage builds make that separation explicit.

The Pattern

Build in one stage, run in another:

FROM node:22-bookworm AS build
WORKDIR /app

COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile

COPY . .
RUN pnpm build
RUN pnpm prune --prod

FROM node:22-alpine AS runtime
WORKDIR /app

COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./package.json

USER node
CMD ["node", "dist/server.js"]

That gives you a clean boundary between "what is required to build" and "what is required to run."

The Alpine Caveat

Alpine can be a good runtime image. It is not automatically the right one.

You still need to think about:

  • native dependencies
  • libc compatibility
  • debugging needs
  • image provenance

Sometimes a slim Debian-based image is the better operational trade. The principle is to ship less, not to cargo-cult a specific base image.

What This Buys You

Done well, multi-stage builds improve:

  • image size
  • cold start and pull time
  • attack surface
  • clarity around runtime dependencies

That is why they are worth doing even when the image size difference is modest.

Further Reading