In this article we are going to cover How to Use Multi-Stage Docker Builds.
In the containerized world, “lightweight” is key. Whether you’re deploying microservices, building a CI/CD pipeline, or packaging a web application, a large, bloated Docker image can slow down deployment and data transfers or even cause security risks. Today, let’s talk about one of Docker’s powerful features, Multi-Stage Build. It helps you build small, efficient, and secure images, so you no longer have to compromise between “functionality” and “size”.
Table of Contents
What is a multi-stage build?
Simply put, a multi-stage build allows you to use multiple FROM commands in a single Dockerfile, where each stage builds independently, handles resources, and ultimately you only “copy” what you need into the final image.
This way, you can Install dependencies and compile code in the first stage.
1. A comparison with Node.js application.
Traditional Dockerfile (Bloat).
Create a file Dockerfile.bloat
.
nano Dockerfile.bloat
Add following code in it.
FROM node:18
WORKDIR /app
COPY . .
RUN npm install && npm run build
CMD ["npm", "start"]
Problems with this Dockerfile:
- The final image contains unnecessary content such as Node.js, build dependencies, source
code, etc. - The image is large and slow to start.
Optimized Multi-Stage Dockerfile
Create a new Dockerfile
.
nano Dockerfile
Add following code in it.
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:18-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm install --only=production
CMD ["node", "dist/index.js"]
Benefits:
- Smaller final image.
- Build tools and development dependencies stripped away.
- More secure and faster deployment.
2. A second comparison with Golang application
Traditional Dockerfile (Bloat)
Create a file Dockerfile.golang.bloat
.
nano Dockerfile.bloat
Add following code in it.
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o app
CMD ["./app"]
Problems:
- is based on the full Golang image, which is large (> 700MB).
- contains unnecessary content such as compilation tools, source code, etc.
Create a file Dockerfile.bloat
.
nano Dockerfile.bloat
Add following code in it.
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /app/app .
CMD ["./app"]
After a multi-stage build, image sizes are typically:
- from ~700MB down to 10-20MB.
- Extreme optimization can even bring a scratch image down to <5MB.
3. Results at a Glance
Run the below command.
docker images | grep -E 'nodeapp|goapp'

What is possible for images with multi-stage builds:
- Reduce build times drastically by using a cache.
- Faster publishing and pulling of images.
- No redundant dependencies, smaller attack surface.
- Separation of stages, easy debugging, and maintenance.
4. Pro Tips for Go Multi-Stage Builds
- CGO_ENABLED=0: turns off CGO support to make executables more cross-platform and dependency-free.
- GOOS / GOARCH: can be built for different platforms, e.g. ARM, Windows, macOS, etc.
- Use scratch or distroless: to further improve security and performance.
Best practices for multi-stage builds
- Use lightweight images (e.g., alpine, slim).
- Reduce system-level dependencies for faster startups.
- Utilize .dockerignore. Ignore unnecessary files, e.g., .git, node_modules, docs, etc.
- Split up phases with clear responsibilities e.g. builder, tester, production for easier debugging and maintenance.
Summary
Multi-stage builds are a must-have tool in every DevOps toolkit. They ensure your images are lean, secure, and fast. With just a few changes to your Dockerfile, you can dramatically reduce image sizes and improve deployment speed.
The next time you write a Dockerfile, remember this: “Build smarter, not bigger.”
Related Articles:
How to push Docker Image to AWS ECR
Reference: