03

Multi-Stage Docker Build

Video: Day 3/40 — Multi Stage Docker Build • https://www.youtube.com/watch?v=ajetvJmBvFo • Duration: ~19 min

Key terms

TermMeaning
Multi-stage buildSeveral FROM stages in one Dockerfile
Build stageThe stage that compiles/produces artifacts
Runtime stageThe final slim image that actually ships
COPY --fromPulls files from an earlier stage
ArtifactCompiled output (binary, bundle) carried forward
Final imageThe last stage, which becomes the shipped image

Problem & solution

Naive Dockerfiles ship compilers, source, and build dependencies in the final image, producing bloated, slow-to-pull images with a large attack surface. We need final images that contain only what's required to run the app.

Solution: Use multiple build stages, compile in a fat toolchain image and copy only the final artifact into a slim runtime image, for small, secure images.

The analogy

When a craftsman builds a product, the workshop crate is full of saws, offcuts, and raw materials; you would never ship that whole mess to the customer. Instead you take just the finished item out and place it in a small, clean finished-goods crate for delivery. A multi-stage Docker build does exactly this: the builder stage holds the compiler and source, and you copy only the artifact into a tiny runtime image that is all that ships.

Problem: fat images

A single-stage build ships build tools + source + dependencies in the final image. Result: huge, slower, larger attack surface.

Solution: multi-stage builds

Use multiple FROM stages. Build in one stage, copy only the final artifact into a tiny runtime stage.

Single-stage vs Multi-stage (ASCII)

A single-stage build keeps everything in one image; a multi-stage build discards the heavy builder and ships only the final artifact.

Graph legend — each node maps to a stage in the Go productcatalogservice build below:

Graph nodeMaps toWhat it does
Single stage - about 1 GBa one-FROM DockerfileShips the whole golang:1.22 toolchain plus source: huge image
Stage 1 build - golang:1.22the FROM golang:1.22 AS build stageCompiles the static server binary
Stage 2 runtime - scratchthe FROM scratch stageCarries only the copied binary, a few MB final image

Example: build a React/Vite app, serve with nginx

Here the app is built with Node, then the static output is copied into a tiny nginx image to serve it.

# ---- Stage 1: build ----
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build          # outputs to /app/dist

# ---- Stage 2: runtime ----
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

Example: Go binary (even smaller)

This builds Google Online Boutique's productcatalogservice — a real Go microservice that serves the product catalog over gRPC on port 3550. A compiled Go binary needs no runtime, so the final stage can be scratch (an empty base) for the smallest possible image.

# productcatalogservice (Online Boutique, Go gRPC service)
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 .

FROM scratch                 # empty base = minimal
COPY --from=build /app/server /server
EXPOSE 3550
ENTRYPOINT ["/server"]

Key flag: `COPY --from=<stage>`

COPY --from=<stage> pulls a file out of an earlier stage into the current one — the heart of multi-stage builds.

  COPY --from=build  /app/server   /server
              ^source stage  ^path in that stage   ^dest in final image

Build & verify size

After building, compare image sizes to confirm the multi-stage version is much smaller than a single-stage build.

docker build -t productcatalogservice:multi .
docker images productcatalogservice    # compare sizes vs single-stage

Benefits

Multi-stage builds win on size, security, and simplicity.

  + Smaller images        -> faster pulls / deploys
  + No build tools shipped -> smaller attack surface
  + One Dockerfile         -> build + package in one place

End-to-end flow

Build heavy, ship light: compile in the builder stage and copy only the artifact into a slim runtime image.

Graph legend — each node maps to the Go productcatalogservice Dockerfile above:

Graph nodeMaps toWhat it does
Source code - productcatalogservicethe Go service repoInput to the build stage
Builder stage golang:1.22FROM golang:1.22 AS buildCompiles the binary with the full toolchain
Artifact /app/servergo build -o /app/server outputThe single static binary that gets carried forward
Runtime stage scratchFROM scratchEmpty final base, nothing but the binary
Slim final imagethe built imageA few-MB image exposing port 3550
Registrydocker push targetStores the slim image for deployment

Key takeaways

  • Name stages with AS <name>, pull artifacts with COPY --from=<name>.
  • Final stage should be a minimal runtime (alpine / scratch / distroless).
  • Build-time deps never reach production.

Checklist

  • [ ] Converted a single-stage Dockerfile to multi-stage
  • [ ] Confirmed final image is dramatically smaller
  • [ ] Understand COPY --from= and named stages