An image is not one solid block. It is a stack of layers, each created by an instruction in the Dockerfile. Understanding layers explains why builds behave the way they do and how to make them fast and small.
When Docker builds an image, most instructions - FROM, RUN, COPY,
ADD - produce a new read-only layer that records the filesystem
changes that instruction made. Stack them and you have the image:
FROM python:3.12-slim # base layers
WORKDIR /app # metadata
COPY requirements.txt . # a layer with one file
RUN pip install -r requirements.txt # a layer with installed packages
COPY . . # a layer with your source
The final image is those layers stacked in order. A running container adds one more, the thin writable layer, on top.
Layers are content-addressed and read-only, so Docker stores each
unique layer once and reuses it everywhere. Two images built on
python:3.12-slim share those base layers on disk rather than
duplicating them. This is why pulling a second image that shares a base
shows "Already exists" for the common layers, and why your total disk
use is far less than adding up every image's reported size.
Every layer adds to the image. A file created in one layer and deleted
in a later layer still occupies space in the first layer - the deletion
just hides it. That is why chaining cleanup into the same RUN matters:
RUN apt-get update && apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
Doing the install and the cleanup in one RUN keeps the leftover
package lists out of the layer entirely, instead of baking them in and
hiding them.
docker history IMAGE lists an image's layers with the instruction and
size of each, which is a good way to spot a surprisingly large step.
docker history IMAGE - show an image's layers and their sizes