Docker multi-stage builds use several FROM instructions in one Dockerfile. Each instruction starts a stage with its own filesystem and purpose. You can compile, test, or package an application in an earlier stage, then copy only the required artifacts into a smaller runtime stage.
This separation keeps compilers, package caches, source files, and test tools out of the final image. The result is usually smaller, easier to inspect, and less exposed to unnecessary software.
A multi-stage build does not automatically make an image secure. You must still choose maintained base images, update dependencies, use a non-root runtime user, and avoid placing secrets in image layers.
How Multi-Stage Builds Work
Every FROM begins a new stage. Name a stage with AS, then use COPY --from to transfer files from that stage. Docker discards files that are never copied into the final stage.
Basic pattern:
# Build stage contains compilers and source files
FROM builder-image AS build
WORKDIR /src
COPY . .
RUN build-command
# Runtime stage receives only the finished artifact
FROM runtime-image AS runtime
COPY --from=build /src/output/app /app
CMD ["/app"]
Create a Multi-Stage Build
The following Dockerfile builds a small Go program. The first stage contains the Go toolchain. The final stage uses a minimal base and receives only the compiled binary.
Dockerfile:
# syntax=docker/dockerfile:1
# Compile a statically linked Linux binary
FROM golang:1.24-alpine AS build
WORKDIR /src
# Cache dependency metadata separately from source changes
COPY go.mod go.sum ./
RUN go mod download
# Copy source and compile the application
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /out/orders-api ./cmd/api
# Keep the runtime image small
FROM alpine:3.22 AS runtime
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
COPY --from=build /out/orders-api /usr/local/bin/orders-api
USER appuser
EXPOSE 8080
ENTRYPOINT ["orders-api"]
Pin versions that match your tested environment. Rebuild regularly so the final image receives current operating-system and runtime fixes.
Build and Run the Final Stage
# Build the final stage and assign a tag
docker build -t orders-api:1.0 .
# Start the container and publish its port
docker run --rm -p 8080:8080 orders-api:1.0
By default, Docker exports the final stage. Earlier stages remain build inputs and do not become part of the final image.
Name Stages Instead of Using Numbers
You can reference stages by zero-based index, but names are clearer and remain correct if you reorder the Dockerfile.
# Give the build stage a stable name
FROM node:24-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Copy output from the named stage
FROM nginx:1.29-alpine AS runtime
COPY --from=build /app/dist /usr/share/nginx/html
Build a Specific Target
Use --target to stop at a named stage. This helps you inspect a builder, run tests in CI, or produce separate debug and production variants.
# Build only through the test stage
docker build --target test -t orders-api:test .
# Build the final production stage
docker build --target runtime -t orders-api:production .
Reuse and Import Stages
A stage can inherit from an earlier named stage, which is useful when debug and production images share a base. COPY --from can also read from an external image; Docker pulls that image when necessary.
# Share common runtime settings
FROM alpine:3.22 AS runtime-base
WORKDIR /app
RUN adduser -D appuser
# Add debugging tools only to this target
FROM runtime-base AS debug
RUN apk add --no-cache curl
# Keep production independent from the debug stage
FROM runtime-base AS production
COPY --from=build /out/orders-api /app/orders-api
USER appuser
CMD ["/app/orders-api"]
Improve Build Cache Use
Docker reuses an unchanged instruction and its dependencies from the build cache. Copy dependency manifests before frequently changing source files so package installation remains cached.
- Place stable, expensive steps before frequently changing steps.
- Use a
.dockerignorefile to keep logs, Git data, dependencies, and local secrets out of the build context. - Use BuildKit cache mounts for package-manager caches when appropriate.
- Copy only the files each stage actually needs.
Example .dockerignore:
# Exclude local and generated files from the build context
.git
node_modules
coverage
*.log
.env
Keep Secrets Out of Image Layers
Do not pass credentials through ARG or ENV during a build. They can appear in image history, provenance, or later layers. Use a BuildKit secret mount instead.
# syntax=docker/dockerfile:1
# Read the token only for this instruction
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
# Supply the secret from a local file without copying it into the image
docker build --secret id=npmrc,src=.npmrc -t web-app:1.0 .
Verify the Final Image
Inspect the size, configuration, and layer history. Run the container as its configured user and test the application. A successful build alone does not prove that the runtime artifact or permissions are correct.
# Review image size and layers
docker image ls orders-api:1.0
docker history orders-api:1.0
# Confirm the configured runtime user
docker inspect --format "{{.Config.User}}" orders-api:1.0
Common Mistakes
| Mistake | Correction |
|---|---|
| Copying the whole build directory | Copy only the final artifacts |
| Installing tools in the runtime stage | Keep build and test tools in earlier stages |
| Using unpinned or abandoned bases | Select maintained, tested image versions |
| Running as root unnecessarily | Create and select a runtime user |
| Passing secrets with ARG | Use BuildKit secret mounts |
Conclusion
Docker multi-stage builds separate the work needed to create an application from the files needed to run it. Name stages, copy only required artifacts, order steps for effective caching, protect build secrets, and verify the final runtime image. This structure produces cleaner Dockerfiles and leaner deployment images without hiding important build steps.