flowchart LR
accTitle: A general mental model for multi-stage builds
accDescr: The build stage prepares application artifacts from source code and build dependencies. Required artifacts are copied into the final stage alongside its runtime and production dependencies.
subgraph build["01. Build stage"]
direction TB
inputs["Source code and<br/>build dependencies"]
prepare["Build or package<br/>the application"]
artifacts["Application artifacts"]
inputs --> prepare --> artifacts
end
subgraph final["02. Final stage"]
direction TB
requirements["Runtime and production<br/>dependencies"]
image["Runnable<br/>application image"]
requirements --> image
end
build -->|"Copy required artifacts"| final
Hands-on Docker (part 2): Save space with multi-stage builds
Multi-stage builds keep build tools and source code out of the image you distribute. For this counter API, the final image was 99.64% smaller in a local build.
A mental model to create a multi-stage build
Separate what you need to build the application from what you need to run it:
“Artifacts” can be compiled binaries, packaged source code, or static assets. The final stage still needs whatever runs or serves them. Docker’s multi-stage build guide explains how stages work.
Implementing a multi-stage build
Continue from Part 1 in the same project directory. Keep the application source, config.yaml, and .dockerignore unchanged. Only the Dockerfile changes.
Before replacing Part 1’s Dockerfile, build its single-stage image as a comparison:
docker build -t my-counter:single-stage .Replace the existing Dockerfile with:
# Build the executable with Rust and Alpine's musl development files.
FROM rust:alpine AS builder
WORKDIR /app
COPY . .
RUN apk add --no-cache musl-dev
RUN cargo install --path .
# Start a new filesystem containing only the application we copy in.
FROM scratch
WORKDIR /app/bin
COPY --from=builder /usr/local/cargo/bin/rust-rocket-counter-api /app/bin/app
ENV ROCKET_ADDRESS=0.0.0.0
CMD ["./app"]AS builder names the first stage. FROM scratch starts an empty filesystem for the final stage. COPY --from=builder transfers the executable into it, without the compiler, source, or build cache.
Build the final stage under a separate tag:
docker build -t my-counter:multi-stage .Command to run the app is:
docker run -p 8000:8000 -v ./config.yaml:/app/config.yaml my-counter:multi-stageUsing config file: config.yaml
Rocket has launched from http://0.0.0.0:8000
The impact of a multi-stage build
List both image tags:
docker image ls my-counterHere is the output from the local comparison:
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
my-counter:multi-stage 55f98349c5cf 5.41MB 0B
my-counter:single-stage 64494a528b90 1.48GB 0B
Both images contain the same executable. The size difference comes from leaving the compiler, source, and build files out of the final image reducing the image size by 99.64%.
Final remarks
Multi-stage builds do not require scratch. Choose a final base image that provides the runtime your application needs, then copy in the build output.
For a Python or Node.js service, keep its language runtime and production dependencies in the final image. Leave build-only tools and files in the builder.