juniorDocker

How do Docker image layers and the build cache work?

Updated Feb 20, 2026

Short answer

Each instruction in a Dockerfile produces a read-only layer, and the image is those layers stacked. On rebuild, Docker reuses a cached layer when the instruction and its inputs are unchanged — but the moment one layer misses, every layer after it is rebuilt. Ordering instructions from least to most frequently changing is therefore the single biggest lever on build time.

Deep explanation

Layers are content-addressed and stacked with a union filesystem. A container adds one thin writable layer on top; everything below is shared between containers and pushed or pulled only once.

The cache rule is what matters in practice: a cache miss invalidates every subsequent layer.

DOCKERFILE
# BAD — source changes bust the dependency install every time
FROM node:20-alpine
WORKDIR /app
COPY . . # any source edit invalidates this layer...
RUN npm ci # ...so this reinstalls everything, every build
CMD ["node", "server.js"]
DOCKERFILE
# GOOD — dependencies cached independently of source
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./ # changes rarely
RUN npm ci # cached unless the lockfile changed
COPY . . # changes constantly, but it is last
CMD ["node", "server.js"]

The only difference is instruction order, and it typically turns a two-minute rebuild into five seconds.

Layers are additive, which surprises people. Deleting a file in a later layer does not reclaim its space — the file still exists in the earlier layer and ships in the image. A secret copied in and then removed is still extractable from the image history.

DOCKERFILE
RUN apt-get update && apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/* # same RUN — the cache never lands in a layer

Splitting that into two RUN instructions would leave the apt lists permanently in the image.

`apt-get update` in its own layer is a classic trap. It gets cached, so weeks later apt-get install runs against a stale package index and either fails or installs outdated versions. Always chain them in one RUN.

Multi-stage builds solve the shipping problem: build in a heavy image with compilers and toolchains, then copy only the artefact into a slim runtime image. The build stage's layers never reach the final image.

Real-world example

A Go service, before and after multi-stage:

DOCKERFILE
FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download # cached until dependencies change
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server ./cmd/server
FROM gcr.io/distroless/static # no shell, no package manager
COPY --from=build /app/server /server
ENTRYPOINT ["/server"]

The final image contains one binary — roughly 15 MB against about 900 MB for the golang base. Nothing from the build stage ships, so the compiler, module cache, and source code are all absent from what runs in production, which cuts both image size and attack surface.

Common mistakes

  • - Copying the whole source before installing dependencies, so every code change reinstalls everything.
  • - Running `apt-get update` in a separate `RUN` from the install, leaving a stale cached package index.
  • - Believing that deleting a file in a later layer shrinks the image — the data remains in the earlier layer and in the image history.
  • - Copying secrets into a layer and removing them later
  • they are still recoverable from the image.
  • - Omitting a `.dockerignore`, so `node_modules` and `.git` are sent to the daemon and bust the cache on every build.
  • - Shipping the build toolchain to production instead of using a multi-stage build.

Follow-up questions

  • Why does one cache miss invalidate all later layers?
  • How do multi-stage builds reduce image size?
  • Why can't you remove a secret by deleting it in a later layer?
  • What does .dockerignore do for build performance?

More Docker interview questions

View all →