An image is not a file. It is not a disk snapshot. It is a manifest — a recipe card that references a stack of content-addressable layers. Understanding this changes how you think about everything Docker does.

1. The Mental Model

Most people think of an image as "a file you download, like an ISO." That model is wrong and will mislead you repeatedly. Here is the correct mental model:

Analogy: An image is a recipe card (the manifest) that references pre-made ingredients (the layers). The recipe card says "stack these ingredients in this order, set oven to 350°F" — it does not contain the ingredients themselves. Two recipe cards can reference the same flour bag.

Concretely, when you docker pull nginx, Docker downloads:

  1. A manifest — a small JSON document (~2 KB)
  2. A config blob — another JSON document with environment vars, entrypoint, etc.
  3. Layer blobs — compressed tar archives of filesystem diffs

The manifest is the image. Everything else is referenced by hash.

2. Content-Addressable Storage

Every layer is identified by its SHA256 hash. The name IS the content. If even one byte changes, the hash changes, and it becomes a different layer.

# Two images sharing a layer — the hash is identical:
sha256:a1b2c3d4...  ← Debian base layer (75 MB)

nginx   → uses sha256:a1b2c3d4... + sha256:e5f6a7b8... + sha256:c9d0e1f2...
node:18 → uses sha256:a1b2c3d4... + sha256:f3a4b5c6... + sha256:d7e8f9a0...

Because both images reference the same Debian base layer hash, Docker stores it once on disk. When you pull the second image, that 75 MB layer is already there — zero network transfer, zero extra disk.

nginx node:18 sha256:a1b2… (Debian) sha256:e5f6… (nginx) sha256:f3a4… (node) Stored ONCE on disk — shared by both images

This principle applies at every level — your local disk, the registry, and network transfers between them. A registry serving millions of images deduplicates aggressively using this mechanism.

🏭 Industry insight: Docker Hub stores tens of millions of images, but the actual layer storage is a fraction of what you'd expect. When you push an image, the registry checks each layer hash — if it already exists, the upload is skipped entirely ("layer already exists"). This is why pushes of rebuilt images are often fast: only the changed layers transfer.

3. Image Manifest

The manifest is the "recipe card." It's a JSON document conforming to the OCI Image Spec:

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "digest": "sha256:9e2b3c...config-hash...",
    "size": 7023
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:a1b2c3d4...base-layer...",
      "size": 32654321
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:e5f6a7b8...app-layer...",
      "size": 1547892
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:c9d0e1f2...config-layer...",
      "size": 423
    }
  ]
}

Key observations:

  • config — points to a JSON blob with env vars, entrypoint, exposed ports, working dir
  • layers — an ordered list. Order matters: layer 0 is the base, each subsequent layer overlays the one below
  • Everything is referenced by digest (content hash). Change the content → different hash → different image
Manifest (JSON document) Config ENV, CMD, EXPOSE Layer 3: config files (423 B) Layer 2: app code (1.5 MB) Layer 1: base OS (32 MB) Manifest references config + ordered layer stack by hash

4. Layers in Detail

Each layer is a tar archive of filesystem changes relative to the layer below. Think of it as a diff — it contains only what was added, modified, or deleted in that build step.

# Layer 1 (base): Debian minimal
/bin/sh
/bin/ls
/usr/lib/...
/etc/passwd
/etc/apt/...

# Layer 2: Install Node.js (adds/modifies files)
/usr/local/bin/node
/usr/local/bin/npm
/usr/local/lib/node_modules/...

# Layer 3: Copy application code
/app/server.js
/app/package.json

# Layer 4: Install dependencies
/app/node_modules/express/...
/app/node_modules/lodash/...

Deletions are represented with special "whiteout" files. If layer 3 removes /tmp/build-cache, the tar contains a file named /tmp/.wh.build-cache — the union filesystem interprets this as "hide that path."

Why this matters: If you install a 200 MB package in layer 3 and delete it in layer 4, the image is still ~200 MB larger than it needs to be. The delete only hides the file — the bytes are still in layer 3. This is why Dockerfile optimization matters (covered in the next lesson).

5. How Images Become Containers

When you run docker run nginx, three things happen:

  1. Layer stacking: The read-only image layers are stacked using a union filesystem (usually overlay2 on Linux). The result looks like a single coherent filesystem.
  2. Writable layer: A thin, empty writable layer is placed on top. All container writes (new files, modifications) go here.
  3. Process launch: The container's entrypoint process starts inside its namespaces, seeing the merged filesystem.

The image is never modified. Multiple containers can share the same image layers simultaneously — each has its own writable layer on top.

Layer 3 (read-only) Layer 2 (read-only) Layer 1 — base (read-only) Container A writable layer Container B writable layer Container C writable layer 3 containers, 1 image — layers shared Each container only uses extra disk for its own writes

This is why starting a container is nearly instantaneous — there's no copying of the image. It's just a mount operation plus a process fork.

6. Image IDs vs Tags

Two completely different concepts that people constantly confuse:

ConceptExampleProperties
Image ID sha256:9e2b3c4d... Content hash. Immutable. If the image changes, the ID changes.
Tag nginx:1.25, nginx:latest Human-readable pointer. Mutable! Can be moved to point at a different ID at any time.
⚠️ Critical: latest is just a tag name. It does NOT automatically mean "the newest version." It's simply the default tag applied when no tag is specified. A maintainer can point latest at any image they choose — or never update it at all.

For reproducible deployments, always reference images by digest:

# Mutable — could change tomorrow:
FROM nginx:1.25

# Immutable — will ALWAYS be exactly this image:
FROM nginx@sha256:9e2b3c4d5e6f7a8b...

7. Inspecting Images

Docker gives you tools to explore everything we've discussed:

# See the layer history (what each layer added)
$ docker history nginx
IMAGE          CREATED       CREATED BY                                      SIZE
9e2b3c4d5e6f   2 weeks ago   CMD ["nginx" "-g" "daemon off;"]                 0B
<missing>      2 weeks ago   EXPOSE map[80/tcp:{}]                            0B
<missing>      2 weeks ago   COPY conf /etc/nginx  # buildkit                 2.3kB
<missing>      2 weeks ago   RUN /bin/sh -c apt-get update && ...            58MB
<missing>      2 weeks ago   ADD debian-slim / # buildkit                     74MB

# Deep inspection — full JSON config, layer digests, env vars
$ docker image inspect nginx

# Remote manifest (without pulling)
$ docker manifest inspect nginx:latest

🧪 Hands-On Task 1: Inspect Image Layers

Do this now:
# Pull nginx and examine its structure
docker pull nginx

# View layer history
docker history nginx

# Inspect full metadata (pipe to less or jq)
docker image inspect nginx | head -80

# Look specifically at the layer digests:
docker image inspect nginx --format '{{json .RootFS.Layers}}' | python3 -m json.tool

Questions to answer:

  • How many layers does the nginx image have?
  • What is the total size vs the largest single layer?
  • What environment variables are baked into the image?

🧪 Hands-On Task 2: Prove Containers Don't Share Writes

Do this now:
# Run two containers from the same image
docker run -d --name box1 nginx
docker run -d --name box2 nginx

# Write a file in box1
docker exec box1 sh -c 'echo "hello from box1" > /tmp/proof.txt'

# Verify it exists in box1
docker exec box1 cat /tmp/proof.txt
# Output: hello from box1

# Prove it does NOT exist in box2
docker exec box2 cat /tmp/proof.txt
# Output: cat: /tmp/proof.txt: No such file or directory

# Clean up
docker rm -f box1 box2

This proves that each container has its own writable layer. The shared image layers below are untouched.

🧠 Quiz Time

🌍 Not Just Docker

OCI Image Specification: Everything described in this lesson — manifests, content-addressable layers, config blobs — is defined by the Open Container Initiative (OCI) Image Spec. It's not proprietary to Docker.

Buildah can create OCI-compliant images without a Docker daemon running. Podman can pull and run them. Skopeo can inspect and copy them between registries. The image format is an open standard.

📋 Key Takeaways

  • An image is a manifest pointing to a config + ordered layers, all referenced by SHA256 hash
  • Content-addressable storage means shared layers are stored once — saving disk and bandwidth
  • Each layer is a tar of filesystem diffs — additions, modifications, and whiteout deletions
  • Containers get a thin writable layer on top of the read-only image stack — the image is never modified
  • Multiple containers share image layers simultaneously — containers are cheap
  • Tags are mutable pointers; image IDs (digests) are immutable content hashes. Use digests for reproducibility.
  • docker history and docker image inspect let you explore all of this yourself