Multi-Stage Docker Build
Video: Day 3/40 — Multi Stage Docker Build • https://www.youtube.com/watch?v=ajetvJmBvFo • Duration: ~19 min
Key terms
| Term | Meaning |
|---|---|
| Multi-stage build | Several FROM stages in one Dockerfile |
| Build stage | The stage that compiles/produces artifacts |
| Runtime stage | The final slim image that actually ships |
COPY --from | Pulls files from an earlier stage |
| Artifact | Compiled output (binary, bundle) carried forward |
| Final image | The 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.
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)
A compiled Go binary needs no runtime, so the final stage can be scratch
(an empty base) for the smallest possible image.
FROM golang:1.22 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server
FROM scratch # empty base = minimal
COPY --from=build /app/server /server
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=builder /app/dist /usr/share/nginx/html
^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 myapp:multi .
docker images myapp # 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.
Key takeaways
- Name stages with
AS <name>, pull artifacts withCOPY --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