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.
| Syntax | Where it appears | What it does |
|---|---|---|
FROM <image> AS <name> | First line of a stage | Opens a stage on the filesystem of a published image and names it |
FROM <stage> AS <name> | First line of a stage | Opens 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 stage | Reads 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 command | Ends 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.
| Content | Present in the builder stage | Present in the runtime stage |
|---|---|---|
| Compiler and language toolchain | Yes | No |
| Source tree and version control metadata | Yes | No |
| Package manager download directory | Yes | No |
| Build output, such as a compiled binary | Yes | Yes, at the path COPY --from wrote it to |
| Files supplied by the runtime base image | No | Yes |
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:latestNo 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.
Related Terms
- Multi stage Docker builds and layer reuse on GitHub Actions: stage layout as a cache decision, and what a job-local builder loses between runs.
- The base image a FROM instruction starts a stage from: how the first line of each stage decides the filesystem that stage builds on.
- Cutting container image size in a GitHub Actions build: the levers besides stage splitting that change what a pushed image weighs.
- Docker multi-stage build documentation: the upstream reference for stage syntax and target selection.
- WarpBuild Docker builders documentation: builder profiles and the actions that point a workflow at a builder.
- WarpBuild caching documentation: cache usage inside GitHub Actions workflows.
- WarpBuild pricing: per minute rates by runner type.
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.