Image layers & internals
How layers stack, cache, and get shared.
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.
$ docker image history payments-api:1.0IMAGE CREATED BY SIZE<missing> CMD ["node" "server.js"] 0B <- metadata onlye5f6a1b2c3d4 COPY . . 1.2MB9d0a1b2c3d4e RUN npm ci --omit=dev 48MB<missing> COPY package*.json ./ 4KBa1b2c3d4e5f6 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.
$ 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.05 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.
$ 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 changedC /tmpA /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.
# BAD: the 400MB archive ships forever, even though it is "removed"RUN curl -o /tmp/data.tar.gz https://example/data.tar.gzRUN 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 behindRUN 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.
# SLOW: source copied before install -> any edit reinstalls depsCOPY . .RUN npm ci# FAST: manifests first -> deps cached until package.json changesCOPY package*.json ./RUN npm ciCOPY . .