CoursesDocker in depthImage management

Image layers & internals

How layers stack, cache, and get shared.

Intermediate14 min · lesson 1 of 30

An image is an ordered stack of read-only layers plus a small JSON config that says how to run it. Every Dockerfile instruction that changes the filesystem — FROM, COPY, ADD, RUN — produces exactly one layer, and each layer is content-addressed by the SHA-256 of its contents. Instructions that only change metadata (ENV, CMD, LABEL, WORKDIR, EXPOSE) add a zero-byte layer that only touches the config. That addressing is the whole trick: identical layers are stored once on disk and pulled once over the network, no matter how many images share them.

IMAGE LAYERS + THE WRITABLE CONTAINER LAYER
Image — read-only layers (shared & cached)
FROM node:22-alpine
base layer, content-addressed by SHA-256
COPY package*.json + RUN npm ci
dependency layer
COPY . .
app source layer
+ config (JSON)
entrypoint, cmd, env, ordered layer digests
Container — writable layer (copy-on-write)
thin writable layer
added on docker run
first write copies the file up
copy-on-write, isolated to this container
discarded on docker rm
changes thrown away
Identical read-only layers are stored on disk and pulled once, no matter how many images share them.
terminal
$ docker image history payments-api:1.0
IMAGE CREATED BY SIZE
<missing> CMD ["node" "server.js"] 0B <- metadata only
e5f6a1b2c3d4 COPY . . 1.2MB
9d0a1b2c3d4e RUN npm ci --omit=dev 48MB
<missing> COPY package*.json ./ 4KB
a1b2c3d4e5f6 FROM node:22-alpine 138MB

The image is layers plus a config object. The config records the runtime defaults baked in at build time — entrypoint, command, environment, exposed ports, working directory, and the ordered list of layer digests (the rootfs). Reading it is how you know what an image will actually do before you run it, which matters when you pull something you did not build.

terminal
$ docker inspect -f '{{json .Config}}' payments-api:1.0 | jq '{Entrypoint,Cmd,User,Env}'
{
"Entrypoint": ["node"],
"Cmd": ["server.js"],
"User": "node",
"Env": ["NODE_ENV=production","PORT=3000"]
}
$ docker image inspect -f '{{len .RootFS.Layers}} layers' payments-api:1.0
5 layers

Copy-on-write and the container layer

When you run a container, Docker adds one thin writable layer on top of the read-only stack. Reads fall through to whichever lower layer owns the file; the first write to any file copies it up into the writable layer before modifying it — copy-on-write. That is why containers start instantly (nothing is copied up front) and why every change a container makes is isolated to that container and thrown away on removal.

terminal
$ docker run -d --name web nginx:1.27
$ docker exec web sh -c 'echo hi > /tmp/note'
$ docker diff web # exactly what the writable layer changed
C /tmp
A /tmp/note # A=added C=changed D=deleted
$ docker rm -f web # the writable layer (and /tmp/note) is gone

Scenario: the image that is mysteriously 900 MB

A teammate’s image is huge and nobody knows why. history is the X-ray: it shows the size each instruction added, and the culprit is almost always a large file created and then deleted in a later instruction — the delete does not shrink the image, because the file still lives in the earlier layer under a whiteout marker. The fix is to create and remove the big thing inside a single RUN so it never becomes its own layer.

Dockerfile
# BAD: the 400MB archive ships forever, even though it is "removed"
RUN curl -o /tmp/data.tar.gz https://example/data.tar.gz
RUN tar xzf /tmp/data.tar.gz -C /opt && rm /tmp/data.tar.gz # too late
# GOOD: download, use, and delete in ONE layer — nothing is left behind
RUN curl -o /tmp/data.tar.gz https://example/data.tar.gz \
&& tar xzf /tmp/data.tar.gz -C /opt \
&& rm /tmp/data.tar.gz

Scenario: every build reinstalls all dependencies

CI complains that builds take four minutes because npm install runs every single time, even for a one-line code change. history reveals the cause: the COPY of source code sits above the dependency install, so any code edit changes that layer and busts the cache for everything below it. Ordering dependency manifests before source restores the cache — deps only reinstall when package.json changes.

Dockerfile
# SLOW: source copied before install -> any edit reinstalls deps
COPY . .
RUN npm ci
# FAST: manifests first -> deps cached until package.json changes
COPY package*.json ./
RUN npm ci
COPY . .
Deleted files still ship
Because layers are stacked and immutable, a file added in one layer and deleted in a later one still exists in the earlier layer, recoverable with docker save and tar. This is why you never COPY a secret in and delete it later (it is fully exposed), and why big intermediate files must be created and removed within a single RUN. A secret in any layer is a secret in the image.