Skip to content

Docker Multi-Stage Builds for Smaller Images

Cut production image size with multi-stage builds — keep build tools out of the final artifact.

1 min read

A Node app with node_modules installed for building can weigh 1 GB. The runtime needs a fraction of that.

Single-stage vs multi-stage image layers

The problem with single-stage builds

Everything used to compile ends up in the image: compilers, dev dependencies, source maps, test files. Most of it does nothing at runtime.

Multi-stage basics

Use multiple FROM statements. Only the final stage ships.

# Stage 1: build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: runtime
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json .
CMD ["node", "dist/index.js"]

The runner stage only contains what it needs. Build tools never reach production.

Use .dockerignore

node_modules
.git
*.log
dist
.env*

Without .dockerignore, COPY . . sends everything to the build context. Large contexts slow every build.

Pin base image versions

FROM node:20.14-alpine3.20 AS builder

Floating tags (node:20) can change between builds. Pin the patch version in CI.

Measure the result

docker image ls your-app

Compare before and after. Multi-stage regularly cuts 60-80% of image size on Node apps.

Keep reading