Chapter 46 — Images and layers: why containers start in milliseconds
In one sentence
A container image is built from a stack of read-only layers, each one a set of filesystem changes, combined at runtime by a union filesystem — and because unchanged layers can be cached and reused across many containers, starting a new container almost never means copying a whole filesystem from scratch.
Why this matters
Understanding layers explains why docker build caching works the way it
does, why images can be surprisingly large or surprisingly small, and why
docker pull only downloads what's actually new.
How it actually works
AN IMAGE is a STACK of LAYERS, each one representing a set of
filesystem CHANGES (files added, modified, or removed) relative to
the layer below it. Every instruction in a Dockerfile that changes
the filesystem (`RUN`, `COPY`, `ADD`) typically creates a NEW layer:
FROM ubuntu:22.04 <- base layers (the OS itself)
RUN apt-get install -y curl <- a new layer: curl's files added
COPY app.py /app/ <- a new layer: your one file added
CMD ["python3", "/app/app.py"] <- metadata only, no filesystem
change, no new layer
THESE LAYERS ARE READ-ONLY and, crucially, CACHEABLE and SHAREABLE:
if two different images both start `FROM ubuntu:22.04`, they can
literally SHARE that base layer on disk -- it's stored ONCE, not
duplicated. When you `docker pull` an image, Docker only downloads
layers it doesn't ALREADY have locally.
A UNION FILESYSTEM (commonly OverlayFS on modern Linux) is what
makes running a container from these layers actually WORK: it
presents the STACK of read-only layers as if they were a SINGLE,
unified filesystem, and adds ONE thin, WRITABLE layer on top,
specific to THIS running container -- any file the container creates
or modifies while running goes into THAT layer, leaving the
underlying image layers completely untouched.
THIS IS EXACTLY WHY CONTAINER STARTUP IS SO FAST: starting a new
container from an image ALREADY ON DISK doesn't require copying
gigabytes of filesystem -- it just needs to set up this thin
writable layer on top of ALREADY-PRESENT, READ-ONLY layers, then
start the process (Chapter 44). It's a direct, practical consequence
of Chapter 15's "renaming/linking is cheap, copying data is
expensive" lesson, applied at the whole-filesystem scale.
Real-world example
"Why did changing ONE LINE in my Dockerfile trigger a rebuild of
EVERYTHING" is a genuinely common, well-documented real question new
Docker users ask, and it's a direct, practical consequence of THIS
chapter's layer-caching mechanism: Docker's build cache works, by
design, LAYER BY LAYER, IN ORDER -- if you change (or invalidate) a
layer, EVERY LAYER AFTER IT in the Dockerfile must be rebuilt too,
even if those later instructions themselves didn't change at all,
because each layer is built ON TOP of the previous one's exact
output.
This is EXACTLY why Docker's own official best-practices
documentation, and essentially every serious Dockerfile style guide,
recommends ORDERING instructions from LEAST-likely-to-change to
MOST-likely-to-change: install your OS packages and dependencies
FIRST (these change rarely), and COPY your actual, frequently-
changing application code LAST. A Dockerfile that does the reverse --
copying application code early, then installing dependencies -- means
EVERY single code change (which might happen dozens of times a day
during active development) invalidates the dependency-installation
layer too, needlessly re-running a slow `apt-get`/`pip install`/`npm
install` step on every single build, turning what should be a
near-instant rebuild into a genuinely painful, minutes-long one. This
real, common performance mistake -- and its well-documented fix -- is
entirely explained by understanding layers as this chapter describes
them, rather than treating Dockerfile ordering as arbitrary.
Try it yourself
# See an image's actual layers
docker pull alpine
docker history alpine
# Build something yourself and watch layer caching in action
mkdir -p ~/lab/dockerdemo && cd ~/lab/dockerdemo
cat > Dockerfile << 'EOF'
FROM alpine:latest
RUN echo "installing dependencies (pretend)" && sleep 2
COPY app.txt /app.txt
CMD ["cat", "/app.txt"]
EOF
echo "version 1" > app.txt
docker build -t layerdemo . # full build, note the timing
echo "version 2" > app.txt
docker build -t layerdemo . # rebuild -- notice the FIRST
# layer (the slow RUN/sleep)
# is CACHED, only the COPY
# layer (and after) rebuilds
# See the difference an image makes on disk
docker images | grep -E "alpine|layerdemo"
Common mistakes
- Putting frequently-changing files (application code) early in a Dockerfile. Invalidates every subsequent layer's cache on every change — order from least-changing to most-changing instead.
- Assuming a container's writable layer changes are permanent. By default, they're LOST when the container is removed — Chapter 49's volumes are the mechanism for data that must actually persist.
- Not realising shared base layers save real disk space and pull time. Standardising on a small number of base images across your organisation is a genuine, practical optimisation, not just tidiness.
Check yourself
- What is a layer, and why are layers cacheable/shareable across images?
- What does the writable layer contain, and what happens to it when a container is removed?
- Why should a Dockerfile install dependencies BEFORE copying application code, in terms of build-cache behaviour?
Further reading
- Docs: Docker's own official "Best practices for writing Dockerfiles" — the layer-ordering guidance directly, from the source.
- Reference:
man 8 mount(the overlay filesystem type), or search "OverlayFS explained" for a clear technical breakdown. - Practice:
docker history <image>on a few real, popular public images — a genuinely instructive way to see real-world layer structure.