Lesson 3 of 30
A production image doesn't need a compiler, a package manager cache, or dev dependencies — it needs the built output and a way to run it. Multi-stage builds are how you ship only that.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
A multi-stage Dockerfile has a `build` stage that installs dev dependencies and a compiler, and a `runtime` stage that uses `COPY --from=build`. What ends up in the final image?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A naive single-stage Dockerfile installs the full toolchain (Node, pnpm, every dev dependency, build tools) and ships all of it in the final image, even though most of that is only needed to produce the build output, not to run it. A multi-stage build splits this into named stages: an early stage does the heavy work with the full toolchain, and a final stage copies only the built output into a minimal runtime image — discarding everything else.
# Stage 1: build — has the full toolchain, discarded at the end
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
# Stage 2: runtime — only what's needed to actually run the app
FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY package.json ./
CMD ["node", "dist/server.js"]
COPY --from=build reaches into a previous stage's filesystem, not the host machine — the build stage's compiler, dev dependencies, and intermediate files never make it into the final image at all. Only the two COPY --from=build lines' worth of output does.
Stage 1 (build): full toolchain, dev dependencies, compiler — large, and never shipped.
FROM nginx:alpine AS runtime is a common pattern for a frontend SPA: a Node build stage produces static files, and the final stage is a lightweight web server image that never had Node installed at all.musl libc instead of glibc, which occasionally causes real compatibility issues with native Node modules compiled against glibc — "distroless" images (Google's minimal, shell-less base images) are a common alternative when Alpine's differences cause friction, trading a slightly larger size for glibc compatibility and an even smaller attack surface (no shell at all).COPY --from references, not from manual configuration.docker system df and docker image ls reveal real, measurable image size — "this image feels big" is a guess; comparing layer sizes before and after a multi-stage refactor is the same falsifiable-check instinct the UX Fundamentals track's whole "audit a real component" lesson argued for, applied to infrastructure instead of a UI component."How would you reduce this Docker image's size?" is a practical, common systems question, and naming multi-stage builds specifically — not just "use a smaller base image" — signals an understanding of why the image is big in the first place: because a single-stage build ships its entire toolchain by default, not just its output.