Originally published on docker.learnio.dev — part of the Docker fundamentals course. View this lesson on the original site.

How layers stack at runtime

An image is a stack of read-only layers. Each running container adds one thin writable layer on top. Most "Docker is weird" moments trace back to that stack.

flowchart TD
    RW["writable container layer"]
    L3["layer 3: app code"]
    L2["layer 2: dependencies"]
    L1["layer 1: base image"]

    RW --> L3 --> L2 --> L1

Two containers from the same image share the lower layers. Only the writable top layer differs per container.

Copy-on-write behaviour

When a process changes a file that came from a lower layer, the engine copies it into the writable layer first, then applies the change. Bytes in the image layers stay untouched.

That explains:

  • why a fresh docker run from an already-pulled image starts quickly
  • why edits in one container do not appear in another
  • why the writable layer is scratch space, not durable storage

Writes that must survive container replacement belong in a volume or external store, not in the container layer.

Prove the writable layer is temporary

Run a container, write a file inside it, remove the container, start again:

docker run --rm --name cw-demo alpine sh -c 'echo inside > /tmp/scratch.txt && cat /tmp/scratch.txt'
docker run --rm alpine cat /tmp/scratch.txt

The second command fails: the file lived only in the first container's writable layer.

Why a later delete does not shrink the image

This Dockerfile looks like it removes build tools:

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y build-essential
RUN apt-get purge -y build-essential && apt-get autoremove -y

The merged filesystem may hide the compiler, but the first RUN layer still contains those bytes. The second layer records a whiteout or deletion in the view; it does not rewrite history.

Keep install, use, and cleanup in one layer when size matters:

RUN apt-get update \
 && apt-get install -y --no-install-recommends build-essential \
 && make -C /tmp/build \
 && apt-get purge -y --auto-remove build-essential \
 && rm -rf /var/lib/apt/lists/*

See layers in the image record

docker pull alpine:3.20
docker image inspect --format '{{len .RootFS.Layers}} layers' alpine:3.20
docker history --no-trunc alpine:3.20

RootFS.Layers is the read-only stack stored for the image. docker history maps Dockerfile instructions to layer entries (newest first).

COPY placement controls rebuild cost. Fat layers often come from split cleanup, not from what docker run shows at runtime. docker exec edits in production are temporary; they do not update the image other containers will use.

Treat image layers as the packaged runtime. Treat the writable layer as disposable state for that one container instance.

From Docker fundamentals — Images, layers, tags, and everyday commands