Skip to content

Multi-Stage Builds

Docker multi-stage builds are a powerful technique to minimize the attack surface of container images by eliminating unnecessary build-time tools and reducing final image size. This approach leverages Docker’s ability to define multiple build stages in a single Dockerfile, allowing you to discard intermediate stages after they’ve served their purpose. By separating compilation and runtime environments, you ensure the final image contains only the essentials required for the application to run.


How Multi-Stage Builds Work

A Dockerfile with multi-stage builds defines multiple FROM statements, each representing a distinct stage. For example:
1. Build stage: Uses a toolchain image (e.g., golang, maven, or node) to compile code.
2. Final stage: Uses a minimal base image (e.g., alpine or scratch) to copy the compiled artifact and configure the runtime environment.

Intermediate stages are automatically discarded during the build process, ensuring they do not contribute to the final image size or attack surface.


Example: Building a Go Application

# Stage 1: Build the Go application
FROM golang:1.21 as builder
WORKDIR /app
COPY . .
RUN go build -o myapp

# Stage 2: Final image using a minimal base
FROM alpine:3.18
WORKDIR /root/
COPY --from=builder /app/myapp .
ENTRYPOINT ["./myapp"]

In this example:
- The builder stage compiles the Go binary.
- The final stage uses alpine to host only the compiled binary, eliminating Go tooling and dependencies.


Benefits of Multi-Stage Builds

  1. Smaller Final Images: Intermediate stages are stripped, reducing the attack surface.
  2. Elimination of Build-Time Tools: Unused compilers, package managers, or debuggers are excluded.
  3. Improved Security: Fewer installed packages and smaller images reduce potential vulnerabilities.

Best Practices

  • Use Minimal Base Images: Prioritize alpine, scratch, or distroless images for the final stage.
  • Clean Up Artifacts: Explicitly remove temporary files or caches in build stages.
  • Leverage .dockerignore: Exclude unnecessary files from being copied into build stages.
  • Keep Stages Focused: Each stage should have a single purpose (e.g., build vs. runtime).

Command Example

To build the example Go application:

docker build -t my-go-app .

This command processes both stages, resulting in a final image based on alpine with only the compiled binary.


Key takeaways

  • Use multi-stage builds to separate compilation and runtime environments.
  • Discard intermediate stages to eliminate unused tools and reduce image size.
  • Prioritize minimal base images like alpine or scratch for the final stage.
  • Combine with .dockerignore and cleanup steps to further minimize the attack surface.
  • Always validate that the final image contains only the necessary components for your application.