Skip to main content
Docker

Docker multi-stage builds: shrink your image without breaking the deploy

Build compilers and development dependencies in one Docker stage, copy only the runtime output into another, and make image size a consequence of a cleaner deployment boundary.

Thien Nguyen
By Thien Nguyen
Updated July 21, 2026 · 2 min read

Shipping TypeScript sources, package-manager caches, compilers, and test tooling inside a production image is easy. It is also a larger attack surface and slower pull for no runtime benefit. Multi-stage builds make the boundary explicit.

The pattern is one build stage carrying everything needed to compile, then a minimal runtime stage that receives only the built application and its production dependencies. Verify that runtime image from scratch—a successful build stage is no proof it contains what production actually needs.

FROM node:22 AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev

FROM node:22-alpine AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
StageContains
BuildSource, compiler, dev dependencies, caches
RuntimeCompiled output and runtime dependencies only

A smaller image is not automatically secure. Keep the base image patched, run as a non-root user, and do not copy .env files or build secrets into either stage.

Use a .dockerignore alongside this pattern: excluding node_modules, .git, test artifacts, and local secrets prevents them from becoming build context in the first place. Multi-stage builds are most useful when they enforce a deployable artifact, not when they become an elaborate size-optimization game.

Keep cache layers useful

Copy dependency manifests before application code so a source-only edit can reuse the dependency-install layer. With BuildKit, mount a package-manager cache for speed, but never copy secrets or credentials from it into the final stage.

COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

Verify the runtime stage on its own

Run the final image with a read-only filesystem and a non-root user. Missing CA certificates, a native module compiled for the wrong libc, or an absent runtime file often hides behind a successful build stage. docker history should show source and compilers only in the build stage; if they appear in runtime, the boundary leaked.

Cover photo by Wolfgang Weiser on Pexels.

References

Primary documentation and specifications checked when this article was last updated.

DockerDevOpsPerformance

Related articles

All articles