This question is testing whether you understand that an image is not a disk image but an ordered list of filesystem diffs — and whether you can explain how that design decision shows up in build cache, pull time, and image size. A strong answer walks from the format to the Dockerfile rule with a concrete example.
The mental model: an image is a manifest of diffs. The OCI manifest lists layers by SHA-256 digest; each layer is a tar of the changes made since the previous layer — deleted files become whiteout markers, not removals. The runtime stacks them into a merged view with overlayfs: the lowerdirs are the read-only image layers, the upperdir is the writable container layer. Writes copy-on-write — the kernel copies a file into the upper layer on open-for-write, and nothing shared is ever modified in place. That is why ten containers from one image cost one copy of the base layers on disk, and why the merged view a container sees is virtual, not real.
Why order matters: the build cache is keyed on layer inputs. A layer whose content and parent digest are unchanged is never rebuilt — Docker and BuildKit return the existing blob instantly. So this Dockerfile burns a CI cycle on every commit:
# bad: any file change invalidates the npm ci layer
COPY . /app
RUN npm ci
# good: dependencies cached, only the app layer rebuilds
COPY package.json package-lock.json /app/
RUN npm ci
COPY src/ /app/srcReorder the lines and the dependency layer's digest stays identical — local builds reuse it, the registry deduplicates it remotely. Layer order is your cache TTL, encoded in a Dockerfile.
The same rule governs pull time. Registries fetch layers in parallel, so a 50-layer image can pull faster wall-clock than a 10-layer image of the same size — but every layer carries per-layer overhead, an HTTP request, decompression, extraction — so hundreds of tiny layers waste time on ceremony. The sweet spot is 10-25 layers with the largest first: a base image like node:20 is ~360 MB compressed, node:20-alpine ~50 MB, and the big layer downloads while small app layers race behind it. And image size is dominated by what's in the diff: build leftovers, cache directories, and source you forgot to .dockerignore stay in the image forever — deleted files don't shrink a layer, they hide in it. Multi-stage builds are the structural fix: the compiler lives in a discarded intermediate stage, and the final image carries only the statically linked binary, typically 90% smaller.
Tradeoffs and edge cases. docker build --squash merges layers and hides secrets left in intermediate layers, but it destroys the cache granularity that makes rebuilds fast and defeats registry-level deduplication — squash for release, never for development. And remember the image sits on top of a shared kernel: layers give you filesystem isolation, not security isolation — containers are process isolation, and the seccomp profile and dropped capabilities are what protect the host.