Reduce Docker image size with multi-stage builds
Large Docker images slow down every build, push and deploy. Multi-stage builds, slim base images and a .dockerignore file can cut an image to a fraction of its size.
A Docker image holds everything a container needs: code, runtime, libraries and system tools. It is easy to let that grow past a gigabyte without noticing. The cost shows up everywhere: slower CI runs, slower deploys, higher registry bills and a larger surface for security issues.
Four changes remove most of the weight.
1. Use a multi-stage build
Compilers, dev dependencies and build caches are needed to build the app, not to run it. A multi-stage build keeps them in a first stage and copies only the result into a clean final image.
# Stage 1: build
FROM node:22 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: run
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]
The final image contains the compiled output and production dependencies. The TypeScript compiler, test tools and source files stay behind in the build stage.
2. Pick a smaller base image
The base image is often the largest layer.
| Base image | Rough size | Good for |
|---|---|---|
node:22 |
About 1 GB | The build stage |
node:22-slim |
About 200 MB | Apps that need glibc |
node:22-alpine |
About 150 MB | Most Node.js services |
| Distroless | Smaller still | Locked down production images |
Alpine uses musl instead of glibc. Native modules sometimes need extra packages there, so test before you switch.
3. Add a .dockerignore file
Without it, COPY . . sends everything in the folder to the build, including node_modules, .git and local environment files.
node_modules
.git
dist
.env*
*.log
This makes builds faster and keeps secrets out of the image.
4. Clean up in the same layer
Each RUN instruction creates a layer. Files deleted in a later layer still take up space in the earlier one. Install and clean up in a single instruction.
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
Check the result
Run docker images to compare sizes before and after, and docker history <image> to see which layer is the heavy one. A Node.js service that started above 1 GB commonly lands under 200 MB after these steps.
Smaller images are half of the story. For the other half, see how to speed up Docker builds with layer caching.