← Recipes

阅读中文版本

Compress a Docker Image with a Multi-Stage Build

Split a Dockerfile into a build stage and a runtime stage, copy only the compiled artifacts, and measure the size reduction before pushing the smaller image.

~30 mindockermultistageoptimization

Task

Reduce a Docker image from over a gigabyte to under 150 megabytes by adopting a multi-stage build and a slim runtime base.

When to use this

The production image ships the full Node toolchain, dev dependencies, and source maps, so it is 1.2 GB and slow to pull. Deploys take six minutes and the registry is filling up. The team wants a small runtime image.

Prerequisites

  • Docker 24 or later with BuildKit enabled
  • A Node.js application that builds with a known command
  • A registry you can push to for the size comparison

Steps

1. Add a build stage that installs all dependencies and runs the build.

FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

Expected output: The build stage produces a dist/ directory with the compiled output.

2. Add a runtime stage that copies only the dist and production dependencies from the build stage.

FROM node:20-slim AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]

Expected output: The runtime stage contains no source, no dev dependencies, and no build toolchain.

3. Build and compare the image sizes before and after the change.

docker build -t app:multistage . && docker images app --format "{{.Size}}"

Expected output: The multistage tag is under 150 MB, down from over 1 GB for the single-stage image.

Troubleshooting

Problem

Runtime crashes with MODULE_NOT_FOUND

Solution

A native dependency was built in the build stage but not copied; install production deps in the runtime stage or copy the specific module.

Problem

The slim base lacks a needed system library

Solution

Switch to the matching debian-slim variant or install the missing library with apt-get in the runtime stage.

Limitations

  • A slim base may lack ca-certificates, breaking TLS to some registries; add it explicitly if needed.
  • Copying node_modules wholesale still includes platform-specific binaries; a per-arch build keeps the image lean.

Two stages, one image

A multi-stage build uses a heavy image to compile and a slim image to run. Only the artifacts you COPY –from=build land in the final image, so the toolchain, dev dependencies, and source never reach the registry.

FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-slim AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]

Measure the win

Build both variants and compare. The size drop is usually an order of magnitude, which means faster pulls, faster deploys, and a registry that stops filling up.

docker build -t app:multistage .
docker images app --format "{{.Size}}"

Expected outcome

The multistage image is under 150 MB, deploys pull in seconds, and the runtime stage contains no source or dev dependencies. If a native module goes missing, install it in the runtime stage rather than copying the whole build tree.