OpsCanary
Back to daily brief
cicdcontainersPractitioner

Mastering Multi-Stage Builds in Docker: Optimize Your Images

5 min read Docker DocsApr 23, 2026
PractitionerHands-on experience recommended

Multi-stage builds exist to tackle the common struggle of optimizing Dockerfiles while maintaining readability and ease of maintenance. They allow you to break down your build process into distinct stages, each with its own base image. This means you can use a heavy image for building your application and a minimal image for the final product, drastically reducing the size of your Docker images.

Each stage begins with a FROM instruction, which can specify different base images. The COPY --from instruction is key here; it lets you pull artifacts from previous stages without carrying over unnecessary files. For example, you can build your application in a golang image and then copy the resulting binary into a scratch image, leaving behind all the build dependencies. This results in a clean, lightweight final image that’s ready for production. You can also specify a target build stage using the --target parameter, which is particularly useful when you want to build only a specific part of your application.

In production, leveraging BuildKit can enhance your build process. With BuildKit enabled, Docker only builds the stages that the target stage depends on, saving time and resources. However, be aware that not all environments may support BuildKit, so always check compatibility. Additionally, while multi-stage builds simplify Dockerfiles, they can introduce complexity if not managed carefully. Ensure your team is aligned on the structure to avoid confusion.

Key takeaways

  • Use multi-stage builds to optimize Dockerfiles for readability and maintainability.
  • Leverage the COPY --from instruction to pull only necessary artifacts into your final image.
  • Enable BuildKit to speed up the build process by only building dependent stages.
  • Specify a target build stage with the --target parameter to focus on specific parts of your application.

Why it matters

In production, smaller Docker images lead to faster deployments and reduced attack surfaces. This efficiency can significantly impact your CI/CD pipeline, leading to quicker iterations and improved reliability.

Code examples

Dockerfile
1# syntax=docker/dockerfile:1
2FROM golang:1.25
3WORKDIR /src
4COPY <<EOF ./main.go
5package main
6import "fmt"
7func main() {
8    fmt.Println("hello, world")
9}
10EOF
11RUN go build -o /bin/hello ./main.go
12FROM scratch
13COPY --from=0 /bin/hello /bin/hello
14CMD ["/bin/hello"]
Bash
$ docker build -t hello .
Dockerfile
1# syntax=docker/dockerfile:1
2FROM golang:1.25 AS build
3WORKDIR /src
4COPY <<EOF /src/main.go
5package main
6import "fmt"
7func main() {
8    fmt.Println("hello, world")
9}
10EOF
11RUN go build -o /bin/hello ./main.go
12FROM scratch
13COPY --from=build /bin/hello /bin/hello
14CMD ["/bin/hello"]

When NOT to use this

The official docs don't call out specific anti-patterns here. Use your judgment based on your scale and requirements.

Want the complete reference?

Read official docs

Test what you just learned

Quiz questions written from this article

Take the quiz →

Get the daily digest

One email. 5 articles. Every morning.

No spam. Unsubscribe anytime.