# Multi-Stage Docker Builds Explained
Multi-stage builds are the cleanest way to produce small, secure container images without sacrificing your build toolchain. If your final image contains a compiler, dev dependencies, or build artifacts you don't need at runtime, multi-stage builds are the fix. Here's how they work and how to use them well.
The problem they solve
Building software often needs heavy tools — compilers, dev dependencies, build caches. But none of that belongs in the image you run in production. A single-stage build bundles everything together: you ship the compiler along with the binary, bloating the image and widening the attack surface.
Multi-stage builds split the process into stages. Early stages do the building; the final stage copies only the finished artifacts into a clean, minimal base. Everything else is discarded.
The basic pattern
A Dockerfile can have multiple FROM statements. Each starts a new stage. You name stages with AS and copy between them with COPY --from=.
# Stage 1: build
FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: runtime — clean base, only what we need
FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/index.js"]The final image contains the production dependencies and the compiled dist/ — but not the dev dependencies or build cache from the build stage.
Where it shines most: compiled languages
For Go, the win is dramatic. The build stage needs the entire Go toolchain (~hundreds of MB); the runtime needs only the single static binary.
FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server ./cmd/server
# Distroless static — no OS, no shell, just the binary
FROM gcr.io/distroless/static-debian12
COPY --from=build /app/server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]This can take a Go image from hundreds of MB down to a handful — just the binary on a near-empty base. CGO_ENABLED=0 produces a static binary so it runs on the minimal distroless base with no libc dependency.
Python example
Python benefits too — build wheels in one stage, install into a clean stage.
FROM python:3.12-slim AS build
WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.12-slim
COPY --from=build /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
WORKDIR /app
COPY . .
USER 1000
CMD ["python", "-m", "app"]Copying the whole virtualenv keeps the final image free of pip's build machinery and caches.
Advanced techniques
Build a specific target
You can build just one stage — useful for a test or lint stage you don't ship.
FROM build AS test
RUN npm test# Run only the test stage in CI
docker build --target test .
# Build the default (final) stage for deploy
docker build -t myapp .Use a shared base stage
Reduce duplication by deriving multiple stages from a common base.
FROM node:20-slim AS base
WORKDIR /app
COPY package*.json ./
FROM base AS deps
RUN npm ci
FROM base AS prod-deps
RUN npm ci --omit=devLeverage build cache mounts
BuildKit cache mounts persist package caches across builds without baking them into layers.
RUN --mount=type=cache,target=/root/.npm npm ciComparison: single vs. multi-stage
| Aspect | Single-stage | Multi-stage |
|---|---|---|
| Image size | Large (tools + artifacts) | Small (artifacts only) |
| Attack surface | Wider | Narrower |
| Build tools in prod | Yes | No |
| Complexity | Lower | Slightly higher |
| Cold start | Slower (bigger pull) | Faster |
Multi-stage builds on PandaStack
PandaStack builds with rootless BuildKit, which fully supports multi-stage builds, --mount=type=cache, and named targets. When you push a repo containing a multi-stage Dockerfile, the platform builds it in an ephemeral Kubernetes Job pod and pushes the final (small) image to Google Artifact Registry, then deploys it via Helm.
The payoff on-platform is concrete: smaller final images pull faster, which means faster deploys and shorter cold starts — particularly valuable for free-tier apps that scale to zero and re-pull on wake. Smaller images also consume fewer build minutes from your plan's monthly budget. If you'd rather not maintain a Dockerfile, PandaStack's buildpack auto-detection produces optimized images for common stacks automatically, but multi-stage gives you maximum control when you want it.
References
- [Docker: Multi-stage builds](https://docs.docker.com/build/building/multi-stage/)
- [Go: Building minimal Docker images](https://docs.docker.com/language/golang/build-images/)
- [Google distroless images](https://github.com/GoogleContainerTools/distroless)
- [BuildKit cache mounts](https://docs.docker.com/build/cache/optimize/)
Push a multi-stage Dockerfile and let PandaStack build and deploy it for you — start free at [dashboard.pandastack.io](https://dashboard.pandastack.io).