Back to Blog
DevOps10 min read2026-07-09

Multi-Stage Docker Builds Explained

Multi-stage builds let you compile in a heavy environment and ship a tiny runtime image. Learn the pattern with real examples for Node, Go, and Python, plus caching and target tricks.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

# 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=dev

Leverage build cache mounts

BuildKit cache mounts persist package caches across builds without baking them into layers.

RUN --mount=type=cache,target=/root/.npm npm ci

Comparison: single vs. multi-stage

AspectSingle-stageMulti-stage
Image sizeLarge (tools + artifacts)Small (artifacts only)
Attack surfaceWiderNarrower
Build tools in prodYesNo
ComplexityLowerSlightly higher
Cold startSlower (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).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in DevOps

Browse all DevOps articles →

See also