What an image actually is: a config plus a stack of content-addressed layers, not a machine snapshot
An image is not a tarball of a running machine — it is a JSON config plus an ordered list of content-addressed read-only layers. The config carries env, entrypoint and rootfs digests; the runtime stacks them and adds one writable layer. This kills the VM-snapshot model.
The registry bill made no sense. The platform team ran 40 microservices, each a ~900 MB Java image, and the math said storage should be 40 × 900 MB = 36 GB per tag generation. The registry reported 4.1 GB. Someone assumed the dashboard was broken and opened a ticket. The answer was in docker history: every one of those 40 images was built FROM eclipse-temurin:21-jre, and that base — the JRE, the Debian userland, the CA certs, ~640 MB of it — was one set of layers with one set of sha256 digests, stored once. Only the thin top layer with each service’s own jar differed, ~40 MB each. The “40 × 900 MB” mental model was the bug: it treated each image as an independent machine snapshot. It is not. An image is a config file pointing at a stack of shared, content-addressed layers — and the registry, the daemon, and the disk all dedup on those digests. The team had been reasoning about images as VMs, and VMs do not share their disk.
In ten minutes you will have the mental model that makes storage estimates, image IDs, and writable containers all snap into place — and you will stop treating images as independent machine snapshots.
What you actually pull
When you docker pull nginx:1.27, you do not download a machine. You download, in order: a small manifest (JSON, a few KB) that lists everything; a config blob (JSON) holding the environment variables, entrypoint, working directory, exposed ports, and the ordered list of layer digests that make up the root filesystem; and then the layers themselves — gzipped tarballs, each named by the sha256 of its contents. Inspect it directly:
$ docker manifest inspect nginx:1.27
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{ "platform": {"architecture":"amd64","os":"linux"}, "digest": "sha256:7f0a...", "size": 2295 },
{ "platform": {"architecture":"arm64","os":"linux"}, "digest": "sha256:e3c1...", "size": 2295 }
]
}That top object is an index — one tag, several per-architecture manifests. The daemon picks the manifest matching your CPU, then pulls that manifest’s config and layers. For nginx:1.27 on amd64 that is 7 layers totalling ~67 MB compressed; the Debian base (~29 MB) and the nginx binaries dominate, and the per-tag application layer is often a few hundred KB.
The config is the image’s identity
Why does changing one environment variable produce a completely new image ID even when no file on disk changed? Because the image ID is not a hash of the layers — it is a hash of the config. The config blob is the part people forget exists. It is plain JSON:
{
"architecture": "amd64", "os": "linux",
"config": {
"Env": ["PATH=/usr/local/sbin:/usr/local/bin:...", "NGINX_VERSION=1.27.0"],
"Entrypoint": ["/docker-entrypoint.sh"],
"Cmd": ["nginx", "-g", "daemon off;"],
"ExposedPorts": {"80/tcp": {}}
},
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:1f3e... ",
"sha256:8a2c... ",
"sha256:b91d... "
]
},
"history": [ { "created_by": "RUN apt-get install ..." } ]
}Two things matter here. First, rootfs.diff_ids is the ordered recipe for the filesystem: stack these layers, bottom to top, and you have the container’s root directory. Second, the sha256 of this config JSON itself is the image ID — the sha256:... you see in docker images --digests. Change one environment variable, rebuild, and the config bytes change, so the image ID changes, even if every layer digest is identical. The image is its config; the layers are shared content the config references.
You rebuild an image after changing only an ENV value in the Dockerfile, and the build hits the cache for every filesystem-modifying step. Does the image ID change?
▸Why this works
Why separate the config from the layers at all? Because it lets one set of layers serve many images. A base layer’s tarball is content-addressed by what is inside it, so two images that share a base reference the same blob digest and the registry stores it once. The config is the cheap, per-image part that varies — different env, different entrypoint, a different top layer — while the expensive filesystem bytes underneath are deduplicated across every image that builds on the same base. Treat the config as identity and the layers as shared content, and the whole storage model falls out.
What a container adds
An image is read-only — every layer is immutable, frozen by its digest. So how does a running container write a log file? When you docker run, the runtime stacks the image’s read-only layers via a union filesystem (overlay2 on Linux) and adds exactly one thin writable layer on top. Writes land there as copy-on-write: modify /etc/hosts and overlay2 copies the file up into the writable layer and edits the copy; the underlying read-only layer never changes. Delete the container and that writable layer is discarded — which is precisely why data you care about must live in a volume, not in the container. The image is the immutable stack; the container is that stack plus one disposable scribble layer.
This is also why “it’s a machine snapshot” misleads. A VM snapshot is a monolithic opaque disk image, copied whole. An OCI image is a structured, addressable, shared artifact: pull deltas (only layers you lack), share bases across hundreds of images, and rebuild deterministically from the config recipe.
Forty services all build FROM the same 640 MB base image. On the registry and on a host that runs all forty, roughly how much does that 640 MB base cost in storage?
- 01List the three kinds of artifact you get from a pull (manifest/index, config, layers) and say what each contains.
- 02Explain why the image ID can change without any layer changing, and why forty images on a shared base cost the base only once.
An image is two things glued together: a config and a stack of layers. The config is plain JSON — Env, Entrypoint, Cmd, ExposedPorts, the working directory, a history array, and crucially rootfs.diff_ids, the ordered list of layer digests that reconstruct the root filesystem. The sha256 of that config JSON is the image ID, which is why changing only an ENV value produces a brand-new image ID even when every filesystem step was a cache hit: the config bytes moved, the layers did not. The layers themselves are gzipped tarballs, each named by the sha256 of its own contents, which makes them content-addressed and therefore shared: forty services built FROM one 640 MB base reference the same layer digests, so the registry and every host store that base exactly once — the “40 × 900 MB” arithmetic that broke the storage estimate was really the VM-snapshot mental model leaking in. A pull is a manifest (or a multi-arch index pointing at per-platform manifests), then the config, then only the layers you lack. At run time the immutable stack gets one thin writable copy-on-write layer on top, where every write lands and which vanishes when the container is deleted — the reason durable state belongs in a volume. Hold the model precisely: config is identity, layers are shared content, the writable layer is a disposable scribble, and none of it is a machine snapshot. Now when you see a storage number that defies arithmetic, check docker history for shared base digests before filing a bug — the dedup is almost certainly already there.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
- Layers and content-addressing: diff_id vs descriptor digest, chain_id, and the gzip non-determinism trapsenior
- The OCI image spec: index, manifest, config and layers as a Merkle DAG of descriptorssenior
- Dockerfile mechanics: which instructions become layers, how the cache keys each step, and why order matterssenior
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.