TL;DR: Every instruction creates a layer; changing one invalidates every layer after it. Put the things that rarely change first, so a code edit does not reinstall dependencies. A multi-stage build compiles in one stage and copies only the artifact into a clean final stage, so build tools never reach production.
How to approach it
Show a bad Dockerfile and the fixed version, because the ordering rule is far clearer as a diff than as a sentence. Then explain multi-stage in terms of what it removes, which is the compiler, the package manager and the source code, since each of those is both weight and attack surface.
A strong answer
An image is a stack of read-only layers, each produced by one instruction. The stack is immutable once built, which is what lets the same image digest be promoted from staging to production rather than rebuilt for each. When a container runs, a thin writable layer goes on top, which is why changes inside a running container vanish when it is replaced.
The cache rule: to reuse a cached layer, the instruction must be identical and every layer beneath it must be unchanged. One edit invalidates everything after it. That is the whole thing to understand.
So this is a common and slow Dockerfile:
FROM node:20
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
CMD ["node", "dist/server.js"]
COPY . . brings in the source, so any code change invalidates that layer, which invalidates npm ci. Every build reinstalls every dependency. Reorder so the dependency manifest is copied on its own:
FROM node:20
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]
Now a code change invalidates only the last two layers. npm ci is reused until the lockfile actually changes, which turns minutes into seconds on most builds.
Two supporting details. A .dockerignore keeps node_modules, .git and local files out of the build context, which otherwise get uploaded to the daemon and can invalidate the COPY layer for reasons nobody can see. And combining related RUN steps matters because a file deleted in a later layer is still present in the earlier one: RUN apt-get install ... followed by a separate RUN rm -rf /var/lib/apt/lists/* leaves the data in the image, so the cleanup belongs in the same instruction.
Multi-stage solves a different problem. The build needs a compiler, a package manager and the source. Production needs none of them.
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/api ./cmd/api
FROM gcr.io/distroless/static:nonroot
COPY --from=build /out/api /api
USER nonroot
ENTRYPOINT ["/api"]
The final image is the binary and nothing else. Only what is explicitly copied from the build stage survives, so the toolchain, the module cache and the source are gone. A Go service goes from around 900MB to roughly 10MB, and the vulnerability count in a scan drops to near zero because there are no OS packages left to be vulnerable.
That size matters operationally rather than aesthetically. Pull time is on the critical path of every scale-up and every node replacement, so a smaller image means faster recovery. The smaller surface also shrinks what a supply chain scan has to reason about, because most findings were OS packages you were never using.
For an interpreted language the same pattern still works: build dependencies and compile assets in the first stage, copy only the installed packages and application into a slim runtime base.
What interviewers probe next
"Why does my image still contain a secret I deleted?" Because deleting it in a later layer does not remove it from the earlier one, and the earlier layer ships. Use a build secret mount, or never let it into a layer.
"What does BuildKit add?" Parallel stage execution, cache mounts that persist a package cache between builds without ending up in the image, and secret mounts. It is the default in current Docker and worth enabling explicitly in CI.
"How do you cache in CI where every build is a fresh machine?" A registry-backed cache, exporting and importing layer cache to the registry. Without it, a clean runner has no cache and the ordering work buys nothing.
Common mistakes
COPY . . before installing dependencies, which defeats the cache on every single build.
Installing packages and cleaning up in separate RUN instructions, so the cleanup adds a layer without removing the weight.
Shipping the build toolchain to production, which is both size and a much larger attack surface.
No .dockerignore, so the build context includes .git and local artifacts and the cache invalidates unpredictably.