Welcome to Day 13 of our Docker adventure! Today, weโll explore multistage builds - a powerful feature that helps us create smaller, more secure Docker images. Weโll transform our previous Go application into a lean, production-ready container using multistage builds.
What are Multistage Builds?
Multistage builds allow us to use multiple FROM
statements in our Dockerfile. Each FROM
instruction starts a new build stage, and we can selectively copy artifacts from one stage to another. This means we can include build tools and dependencies in earlier stages without bloating our final image.
Before: Single Stage Build
Letโs look at our previous Go application Dockerfile:
FROM golang:latest
WORKDIR /app
COPY . .
RUN go build -o main .
EXPOSE 8080
CMD ["./main"]
This Dockerfile works, but it includes the entire Go toolchain and build dependencies in our final image. Letโs check its size:
$ docker build -t hello-world-go .
$ docker images hello-world-go
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world-go latest 6209fff2e822 2 days ago 928MB
Nearly 1GB for a simple Go application! ๐ฑ
After: Multistage Build
Hereโs how we can improve it using multistage builds:
# Build stage
FROM golang:latest AS builder
WORKDIR /app
COPY . .
RUN go build -o main .
# Final stage
FROM alpine:3.18
WORKDIR /app
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
Letโs break down whatโs happening:
- The first stage (named
builder
) uses the full Go image to compile our application - The second stage starts fresh from a minimal Alpine Linux image
- We copy only the compiled binary from the builder stage
- Everything else from the build stage is discarded
Now letโs check the size:
$ docker build -t hello-world-go-multistage .
$ docker images hello-world-go-multistage
REPOSITORY TAG IMAGE ID CREATED SIZE
hello-world-go-multistage latest dc5a7b64247a 6 seconds ago 14.9MB
Thatโs a 98% reduction in size! ๐
More Examples
Multistage builds are not only useful for Go applications. They can be applied to any language or framework. Here are some more examples:
Node.js Application
Hereโs how a multistage build looks for a Node.js static website (like adventofdocker.com):
# Build stage - compiles the application
FROM node:lts AS base
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# Runtime stage - serves the static files
FROM nginx:mainline-alpine-slim AS runtime
COPY --from=base ./app/dist /usr/share/nginx/html
EXPOSE 80
This example shows how to build and serve a static website using two stages:
- The build stage uses Node.js to install dependencies and build the application
- The runtime stage uses a lightweight nginx image to serve only the built static files
This approach significantly reduces the final image size by excluding Node.js, npm, and all development dependencies from the runtime image.
Benefits of Multistage Builds
- Smaller Image Size: Final images contain only whatโs necessary to run the application
- Better Security: Fewer components mean a smaller attack surface
- Faster Deployments: Smaller images are faster to push and pull
- Clean Separation: Build-time dependencies are completely separated from runtime
Best Practices
- Name your build stages for better readability
- Use specific base image versions instead of
latest
(e.g.FROM node:20
instead ofFROM node:latest
) - Keep frequently changing files (like source code) in later
COPY
commands - Consider using distroless or minimal base images for the final stage
Conclusion
Multistage builds are a game-changer for containerized applications. They help us maintain lean, secure, and efficient Docker images without compromising our development workflow. Whether youโre working with Go, Node.js, Java, or any other language, multistage builds should be part of your Docker best practices!
Tomorrow, weโll explore common debugging techniques to help you solve issues faster. Until then, happy containerizing! ๐ณ
Jonas