OpsCanary
cicdcontainersPractitioner

Mastering Multi-Stage Builds in Docker: Optimize Your Images

5 min read Docker DocsMay 17, 2026Reviewed for accuracy
Share
PractitionerHands-on experience recommended

Multi-stage builds exist to solve the common pain point of bloated Docker images. They allow you to break down your build process into distinct stages, each with its own base image. This means you can compile your application in one stage and only copy the final artifact to a minimal base image, significantly reducing the size and complexity of your Docker images.

The process begins with the FROM instruction, which starts a new build stage. Each FROM can use a different base, allowing for flexibility in your build process. For example, you can use a full-fledged development image to build your application and then switch to a scratch image for the final stage. The COPY --from instruction is crucial here; it enables you to copy artifacts from previous stages without carrying over unnecessary dependencies. This results in a clean, lightweight final image that contains only what you need to run your application.

In production, you need to be aware of how to effectively use multi-stage builds. For instance, enabling BuildKit with the DOCKER_BUILDKIT=1 environment variable can enhance your build performance and capabilities. Be mindful of the stages you define; using the --target flag allows you to specify which stage to build, which can be useful for testing or debugging. However, remember that while multi-stage builds are powerful, they can also introduce complexity if not managed properly. Keep your Dockerfiles readable and maintainable to avoid confusion down the line.

Key takeaways

  • Utilize multi-stage builds to reduce Docker image size by copying only necessary artifacts.
  • Leverage the COPY --from instruction to bring in built artifacts from previous stages.
  • Enable BuildKit for improved build performance and capabilities.
  • Use the --target flag to specify which build stage to execute, aiding in testing and debugging.
  • Maintain readability in your Dockerfiles to prevent confusion in complex builds.

Why it matters

In production, smaller Docker images lead to faster deployments and reduced resource usage. This efficiency can significantly impact your CI/CD pipeline and overall application performance.

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 →
RailwaySponsor

Deploy any app in seconds — no infrastructure config, no DevOps overhead. Instant deployments from GitHub, built-in databases, and automatic scaling.

Start deploying free →

Get the daily digest

One email. 5 articles. Every morning.

No spam. Unsubscribe anytime.