← Recipes

阅读中文版本

Create a Multi-Stage Dockerfile for Smaller Images

Use multi-stage Docker builds to compile your application in one stage and copy only the runtime artifacts into the final small image. This pattern can reduce image sizes by 10x or more.

~15 mindockerdevopsdeploymenttypescriptnode

Task

Build a Node.js application with a multi-stage Dockerfile that produces a minimal production-ready image under 50MB.

When to use this

You have a Node.js backend using TypeScript. The Docker image built with the standard pattern is 1.2GB because it includes devDependencies, the TypeScript compiler, source maps, and build tooling. You want to reduce attack surface and deployment time.

Prerequisites

  • Docker installed (Docker Engine 20.10+ or Docker Desktop 4.0+)
  • Node.js project with a package.json and TypeScript config
  • Basic familiarity with Dockerfile syntax

Steps

1. Create a Dockerfile with a build stage that compiles the TypeScript source. Use a Node image with the build toolchain.

# ---- Build Stage ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --include=dev
COPY tsconfig.json ./
COPY src ./src
RUN npm run build

Expected output: Docker builds the image, installs all dependencies (including dev), and runs the TypeScript compiler to produce JavaScript output in /app/dist.

2. Create a production stage that copies only runtime dependencies and the compiled output. Use the Alpine or Distroless variant for the smallest footprint.

# ---- Production Stage ----
FROM node:20-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]

Expected output: Docker creates the production image with only production dependencies (~30MB) and the compiled JS (the TypeScript compiler and source are NOT present).

3. Build the image and verify the size reduction.

docker build -t myapp:latest . && docker images myapp:latest

Expected output: The final image should be 40-60MB compared to 1.2GB (build+dev+deps). Typical reduction: 20-30x for Node.js apps.

4. Verify that the application starts correctly.

docker run --rm -p 3000:3000 myapp:latest

Expected output: Container starts, listens on port 3000, and responds to HTTP requests.

Troubleshooting

Problem

COPY --from=builder fails with file not found

Solution

Ensure the source path matches the BUILD output path. If your tsconfig outputs to ./dist, copy /app/dist not /app/build.

Problem

Container exits immediately with 'node: not found'

Solution

The production image may lack Node.js if you used Distroless. Use node:20-alpine or gcr.io/distroless/nodejs20-debian12 which includes the Node runtime.

Problem

npm ci fails with integrity check errors

Solution

Ensure package-lock.json is committed and matches package.json. Delete node_modules and lock file, regenerate with npm install.

Limitations

  • Multi-stage builds do not help if your production dependencies themselves are large. Use OCI image squashing or third-party tools like docker-slim for further reduction.
  • BuildKit (Docker Buildx) is required for some advanced multi-stage features like --mount=type=cache.

Why Multi-Stage Builds

A standard single-stage Dockerfile copies everything into one image: source code, build tools, devDependencies, cache files. The resulting image carries substantial attack surface and wastes bandwidth during deployment.

Multi-stage builds let you keep the build tooling in one stage and copy only the build artifacts into a clean, slim production stage.

How Multi-Stage Builds Work

Each FROM instruction starts a new stage. You can refer to earlier stages by name (AS builder) or index (--from=0). Only the last stage is included in the final image (unless you target a different stage with --target).

Advanced Patterns

Cache Mounts for Faster Builds

Use BuildKit cache mounts to persist the npm cache across builds:

RUN --mount=type=cache,target=/root/.npm \
    npm ci --include=dev

Targeted Build for Development

FROM builder AS development
CMD ["npm", "run", "dev"]
# Usage: docker build --target development -t myapp:dev .

Security Scanning

After building, scan the minimal image for CVEs:

docker scout cves myapp:latest

A smaller image with fewer packages has fewer potential vulnerabilities.