Key idea
An image is a stack of read-only layers. Each instruction in a Dockerfile that changes files adds one layer on top of the last. When you build again, Docker reuses every layer whose inputs haven't changed, and rebuilds from the first one that has.
One instruction, one layer
A Dockerfile is the recipe an image is built from, one instruction per line. You'll only need four to read this lesson: FROM picks the image to start from, COPY copies files in, RUN runs a command while building, and CMD names what the container runs. Anatomy of a Dockerfile covers them all.
Here's a small Node app's Dockerfile:
# The base image's layers
FROM node:22-alpine
WORKDIR /app
# A layer with one file
COPY package.json ./
# A layer with node_modules
RUN npm install --omit=dev
# A layer with your code
COPY . .
# Settings only, no files
CMD ["node", "server.js"]
Stacked, the layers make the file system the container sees. A layer only records what changed: files added, changed or deleted.
The build cache
Build the image twice without changing anything, and the second build says CACHED for every step. Change server.js and build again:
#6 [2/5] WORKDIR /app
#6 CACHED
#7 [3/5] COPY package.json ./
#7 CACHED
#8 [4/5] RUN npm install --omit=dev
#8 CACHED
#9 [5/5] COPY . .
Only the last COPY runs again, because it's the first step whose input changed. Everything after a changed step rebuilds too, even if its own input didn't change.
Why order matters
Put what changes rarely at the top and what changes often at the bottom. Swap the order, and copy all the code before installing:
COPY . .
RUN npm install --omit=dev
Now every one-line code change invalidates the COPY, so npm install runs again from scratch on every build. On a real app that's minutes instead of seconds.
The rule is the same in every language: copy the dependency list (package.json, requirements.txt, go.mod) and install, then copy the code.
Layers are shared
Two images built on the same base share its layers, on disk and in a download: Docker only fetches layers it doesn't already have. Ten services on node:22-alpine store that base once. A new version of your app is a small download for the same reason: only the layers that changed.
Deleting doesn't shrink
A later layer can hide a file, but the earlier layer still holds it. Copy in a 500 MB file and delete it in the next step, and the image is still 500 MB bigger. The same goes for a secret: anyone who pulls the image can read it from the old layer, as How secrets leak warned.
When a container runs, it gets one thin writable layer of its own on top of the stack. That layer goes away with the container, which The container filesystem is temporary picks up.
Check yourself