Multi Stage Build

A multi stage Docker build is a Dockerfile with several FROM stages, where a later stage copies only the artifacts it needs out of an earlier one.

A multi stage Docker build is a Dockerfile that contains several FROM instructions, each of which opens a new stage, where a later stage copies only the artifacts it needs out of an earlier one with COPY --from. Everything the earlier stage used to produce those artifacts, such as the compiler, the source tree, and the package manager download directory, stays behind in a stage that is never part of the exported image.

The pattern exists because building software needs tools that running it does not. A single stage Dockerfile ships both, since every instruction in the file adds a layer to the one image the file produces. Splitting the file into stages lets the build keep its toolchain while the image keeps only its output.

Definition

A stage is the span of a Dockerfile from one FROM instruction up to the next FROM or the end of the file. Stages are numbered from zero in the order they appear, and AS gives a stage a name that later instructions can reference. Docker has supported the syntax since Engine 17.05 (multi-stage builds documentation, checked on 2026-08-13).

Four pieces of syntax carry the whole pattern.

SyntaxWhere it appearsWhat it does
FROM <image> AS <name>First line of a stageOpens a stage on the filesystem of a published image and names it
FROM <stage> AS <name>First line of a stageOpens a stage on the filesystem an earlier stage ended with, so the new stage inherits everything that stage installed
COPY --from=<stage> <src> <dst>Inside a later stageReads the named paths out of another stage and writes them into the current one. Nothing beyond those paths crosses over
--target <stage>On the build commandEnds the build at the named stage instead of the last stage in the file

What ends up in the image

The exported image is the stage the build ended at. Its layers are the instructions of that stage plus the layers of whatever it started FROM. Earlier stages contribute exactly the paths a COPY --from named and nothing else, which is what separates a build stage from the image that ships.

ContentPresent in the builder stagePresent in the runtime stage
Compiler and language toolchainYesNo
Source tree and version control metadataYesNo
Package manager download directoryYesNo
Build output, such as a compiled binaryYesYes, at the path COPY --from wrote it to
Files supplied by the runtime base imageNoYes

Build secrets follow the same boundary. A credential mounted or copied into an early stage is absent from the runtime stage unless a COPY --from names its path, so the secret never reaches a layer that gets pushed to a registry.

Which stages actually run

A build does not execute every stage in the file. The builder resolves the stage it was asked for, walks back through the stages that one inherits from or copies out of, and runs only those. A stage the selected target never reaches is skipped entirely, along with any downloads it would have performed.

That turns --target into a way to get several images out of one Dockerfile. A test stage carrying the test runner and a release stage carrying the binary can both descend from the same dependency stage, and each build reaches only the branch it asked for.

Copying from outside the file

COPY --from also accepts an image reference in place of a stage name, so a stage can lift one file out of a published image without inheriting that image's layers. Writing COPY --from=alpine:3.20 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ puts a certificate bundle into a base image that ships without one, and nothing else from the alpine image comes along (Dockerfile reference, checked on 2026-08-13).

Stages form a graph

Stages are a dependency graph rather than a sequence, even though the file reads top to bottom. Two stages that reference nothing from each other have no edge between them, and a builder with spare capacity runs them at the same time. A stage that copies from another waits for that one to finish.

Example

A Go service compiled in one stage and shipped from another. The builder stage holds the toolchain and the downloaded modules; the final stage starts from a minimal base image and receives a single file.

# syntax=docker/dockerfile:1

FROM golang:1.24-bookworm AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -o /out/server ./cmd/server

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /out/server /usr/local/bin/server
USER nonroot
ENTRYPOINT ["/usr/local/bin/server"]

The final stage runs three instructions. It has no /src, no Go module cache under /go/pkg/mod, and no compiler, because the only path crossing the stage boundary is /out/server. The distroless base it starts from ships without a shell and without a package manager (distroless project, checked on 2026-08-13), so the running container holds the binary plus the certificates and timezone data that base image provides.

The workflow below builds that Dockerfile on a GitHub Actions runner.

name: image
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: false
          tags: example/server:latest

No target input is set, so the build ends at the last stage in the file and the runtime image is what comes out. Adding target: builder to the same step ends the build one stage earlier and produces an image containing the toolchain and the compiled binary, which helps when a compile failure needs an interactive shell and is the wrong thing to push to a registry.

Two behaviors are worth tracing across pushes of this workflow. A commit that touches only cmd/server leaves the go.mod and go.sum copy above it unchanged, so go mod download can be reused while the compile reruns. A commit that adds a module changes the lockfile copy and reruns both.

Whether that reuse survives from one workflow run to the next is a separate question from the Dockerfile. docker/setup-buildx-action creates a builder inside the job, and that builder is discarded when the job ends, so the builder stage starts cold on the next push unless the build imports layer cache from a store that outlives the job or runs on a builder whose storage does. Stage layout decides how much work a cache hit can save; the builder's lifetime decides whether the hit happens at all.

FAQ

How many stages can one Dockerfile have?

There is no fixed limit. Every FROM instruction opens a stage, stages are numbered from zero in the order they appear, and a stage can be given a name with AS so that later instructions reference it by name instead of by index.

Which stage becomes the image when no target is passed?

The last stage in the file. Passing a target ends the build at the named stage instead, which is how one Dockerfile produces a test image and a runtime image from the same set of earlier stages.

Can COPY --from read from an image instead of a stage in the file?

Yes. COPY --from accepts an image reference as well as a stage name, so a stage can lift one file out of a published image without inheriting that image's layers. Copying a certificate bundle into a minimal base image is the common case.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.