Docker Multi-stage Builds Reference

Create efficient, secure, and compact Docker images with multi-stage builds

Syntax

FROM <image> [AS <stage>]
COPY --from=<stage> <src>... <dest>

Multi-stage builds allow you to use multiple FROM statements in a single Dockerfile, with each FROM instruction starting a new build stage. You can selectively copy artifacts from one stage to another, leaving behind everything you don't need in the final image.

Multi-stage builds are a feature in Docker that allow you to create more efficient and smaller Docker images by separating build-time dependencies from runtime dependencies. This approach helps solve a common challenge in container development: how to keep container images small and secure while still including all necessary tools for building the application.

With multi-stage builds, you can use multiple FROM statements in your Dockerfile. Each FROM instruction can use a different base image, and each one begins a new stage of the build. You can selectively copy artifacts from one stage to another, leaving behind everything you don't want in the final image.

How Multi-stage Builds Work

A multi-stage build Dockerfile contains multiple FROM instructions, each defining a new build stage with its own filesystem. The stages are isolated from each other, but files can be copied between stages using the COPY --from instruction.

The final FROM instruction defines the base image for the final container image. Any previous stages are considered intermediate stages and can be referenced using the AS name in the FROM instruction.

Multi-stage Build Diagram

The key components of multi-stage builds include:

Named Stages

You can assign names to build stages for easier reference:

FROM golang:1.18 AS builder

This creates a stage named "builder" based on the golang:1.18 image.

Copying Between Stages

You can copy files from one stage to another using the COPY instruction with the --from flag:

COPY --from=builder /go/src/app/myapp /usr/local/bin/

This copies the myapp binary from the "builder" stage to the /usr/local/bin/ directory in the current stage.

Target Stages

You can build up to a specific stage by using the --target flag with docker build:

docker build --target builder -t myapp:builder .

This builds only up to the "builder" stage and tags the resulting image as myapp:builder.

Examples of Multi-stage Builds

Basic Multi-stage Build for Go

A simple multi-stage build for a Go application:

# Stage 1: Build the application
FROM golang:1.18 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp -a .

# Stage 2: Create the minimal runtime image
FROM alpine:3.16
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/myapp .
CMD ["./myapp"]

This example uses a large golang image to build the application, but the final image is based on a minimal Alpine Linux image, containing only the compiled binary and required certificates.

Multi-stage Build for Node.js

A multi-stage build for a Node.js application with a build step:

# Stage 1: Build the application
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Create the production image
FROM node:18-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]

This example builds a Node.js application in the first stage and then creates a production image with only the runtime dependencies and compiled code.

Multi-stage Build for a Frontend Application

A multi-stage build for a React/Vue/Angular frontend application:

# Stage 1: Build the frontend assets
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: Create the web server image
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

This example builds a frontend application in the first stage and then copies only the compiled static assets to an Nginx web server image.

Advanced Multi-stage Build with Multiple Sources

A multi-stage build that copies from multiple sources:

# Stage 1: Build the backend
FROM golang:1.18 AS backend-builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY backend/ ./
RUN go build -o api-server .

# Stage 2: Build the frontend
FROM node:18 AS frontend-builder
WORKDIR /app
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build

# Stage 3: Create the runtime image
FROM alpine:3.16
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=backend-builder /app/api-server /app/
COPY --from=frontend-builder /app/build /app/public
EXPOSE 8080
CMD ["./api-server"]

This example builds a backend and frontend in separate stages, then combines them in a final minimal image.

Using External Images as Stages

Copying from external images in a multi-stage build:

# Use an external image as a stage
FROM alpine:3.16
WORKDIR /app
COPY --from=busybox:1.35 /bin/busybox /bin/
COPY --from=nginx:alpine /etc/nginx/nginx.conf /etc/nginx/
CMD ["busybox", "httpd", "-f", "-p", "8080"]

This example copies files from external images without having to define them as explicit stages in the Dockerfile.

Using Build Arguments with Multi-stage Builds

Using build arguments to control which stages are used:

# Use build arguments to control stages
ARG BUILD_IMAGE=golang:1.18
ARG RUNTIME_IMAGE=alpine:3.16

FROM ${BUILD_IMAGE} AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .

FROM ${RUNTIME_IMAGE}
WORKDIR /app
COPY --from=builder /app/myapp .
CMD ["./myapp"]

This example uses build arguments to control which base images are used for the build and runtime stages, allowing for flexibility at build time.

Best Practices for Multi-stage Builds

  • Use specific base image tags: Always use specific version tags for your base images to ensure consistent builds.
  • Name your stages: Use meaningful names for your build stages to make the Dockerfile more readable and maintainable.
  • Order stages by stability: Place stages that change less frequently earlier in the Dockerfile to maximize cache usage.
  • Minimize the number of COPY instructions: Group related files in a single COPY instruction to reduce the number of layers.
  • Use .dockerignore: Create a .dockerignore file to exclude files that aren't needed in the build context.
  • Clean up within each stage: Remove temporary files and build artifacts within each stage to keep intermediate stages clean.
  • Copy only what you need: Be specific about what files you copy between stages to keep the final image small.
  • Use the smallest possible base image for your final stage (e.g., alpine, distroless, or scratch).
  • Consider security: Use multi-stage builds to avoid including build tools and credentials in the final image.
  • Leverage BuildKit's cache mounts for package managers in build stages to speed up builds.

Advanced Techniques

Building from a Specific Stage

You can build your Docker image up to a specific stage using the --target flag:

docker build --target builder -t myapp:builder .

This is useful for:

  • Debugging a specific build stage
  • Creating development images with additional tools
  • Running tests in a build stage without proceeding to the final image
Using Previous Stage Numbers

If you don't name your stages, you can refer to them by their index in the COPY --from instruction:

# First stage (index 0)
FROM golang:1.18
WORKDIR /app
COPY . .
RUN go build -o myapp .

# Second stage (index 1)
FROM alpine:3.16
WORKDIR /app
# Copy from the first stage (index 0)
COPY --from=0 /app/myapp .
CMD ["./myapp"]

However, using named stages is generally more readable and maintainable.

Using BuildKit Cache Mounts

Docker BuildKit (enabled with DOCKER_BUILDKIT=1) provides additional features for multi-stage builds, including cache mounts:

FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
# Use a cache mount to persist the npm cache between builds
RUN --mount=type=cache,target=/root/.npm \
    npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html

This can significantly speed up builds, especially for stages with package managers that maintain their own caches.

Parallel Building with BuildKit

BuildKit can build independent stages in parallel, speeding up the overall build:

# These stages can be built in parallel with BuildKit
FROM golang:1.18 AS backend
WORKDIR /app
COPY backend/ .
RUN go build -o api-server .

FROM node:18 AS frontend
WORKDIR /app
COPY frontend/ .
RUN npm ci && npm run build

# This stage depends on both previous stages
FROM alpine:3.16
COPY --from=backend /app/api-server /app/
COPY --from=frontend /app/build /app/public
CMD ["./app/api-server"]

Enable BuildKit by setting the DOCKER_BUILDKIT=1 environment variable when running docker build.

Using ARG in FROM

You can use build arguments to dynamically select base images for different stages:

ARG GO_VERSION=1.18
FROM golang:${GO_VERSION} AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp .

ARG RUNTIME_VERSION=3.16
FROM alpine:${RUNTIME_VERSION}
COPY --from=builder /app/myapp /app/
CMD ["/app/myapp"]

This allows you to customize the build without modifying the Dockerfile.

Common Use Cases for Multi-stage Builds

Compiled Languages

Multi-stage builds are ideal for compiled languages like Go, Rust, C++, and Java, where you need a full development environment for compilation but only the compiled binary for runtime.

# Java application with multi-stage build
FROM maven:3.8-openjdk-17 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src/ /app/src/
RUN mvn package -DskipTests

FROM openjdk:17-jre-slim
COPY --from=builder /app/target/*.jar /app/app.jar
CMD ["java", "-jar", "/app/app.jar"]
Frontend Applications

Multi-stage builds work well for frontend applications that need a build step (like React, Vue, or Angular) but then only need to serve static files at runtime.

# React application with multi-stage build
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
Separating Test and Production

You can use multi-stage builds to run tests in an intermediate stage but exclude the test code and dependencies from the final image.

# Stage for testing
FROM node:18 AS tester
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm test

# Stage for building
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:18-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --only=production
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]
Creating Minimal Secure Images

Multi-stage builds can create minimal images that contain only what's needed for runtime, reducing attack surface and image size.

# Build stage
FROM golang:1.18 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp .

# Create a minimal image from scratch
FROM scratch
COPY --from=builder /app/myapp /
CMD ["/myapp"]

Notes and Limitations

  • Multi-stage builds require Docker 17.05 or newer. If you're using an older version, you'll need to use alternative approaches like shell scripts to achieve similar results.
  • Each stage in a multi-stage build has its own independent filesystem. Files from previous stages are not automatically available in subsequent stages unless explicitly copied with COPY --from.
  • Only the final stage in a multi-stage build contributes to the final image. Intermediate stages are used during build but don't affect the final image's size unless files are copied from them.
  • The --target flag only builds up to the specified stage, but still uses all prior stages in the process. It doesn't change how the final image is built.
  • BuildKit features like cache mounts, parallel building, and advanced caching require BuildKit to be enabled (DOCKER_BUILDKIT=1 or Docker version 23.0 or later).
  • External images used with COPY --from must be pulled during the build process, which can impact build time if they're large or not cached locally.
  • You can use up to 125 total stages (0-124) in a multi-stage build, though most Dockerfiles only need a few stages.