Anatomy of a Docker Image: Layers, Config JSON, and Manifest Specifications
Understanding content-addressable storage digests, multi-arch manifests, and whiteout files.
Part 5 in Series — Catch up on the previous article: Building a Container From Scratch in Linux: unshare, chroot, pivot_root, and cgroups (Part 4) before diving into this post.
A developer modifies a single line of code in a microservice application and pushes a git commit to CI/CD.
The build pipeline triggers docker build.
Instead of uploading a 600 Megabyte archive across the network to the container registry, the build completion log displays:
Layer 1: Using cache (75 MB)
Layer 2: Using cache (250 MB)
Layer 3: Pushed (3.2 MB)
Digest: sha256:4f8e9a0...
The deployment completes in 3 seconds.
How does Docker know that 325 Megabytes of the image remained identical, allowing it to push only a tiny 3.2 Megabyte delta?
To understand image layer efficiency, content-addressable storage, and build speed optimization, we must dissect the internal filesystem anatomy of a Docker image.
1. High-Level Image Anatomy
A Docker image is not a single monolithic binary or virtual machine disk image.
It is a content-addressed bundle comprising three main components:
+-------------------------------------------------------------------+
| 1. Manifest List / Index |
| Selects platform architecture (e.g., linux/amd64 vs linux/arm64)|
+-------------------------------------------------------------------+
|
v
+-------------------------------------------------------------------+
| 2. Image Manifest JSON |
| Maps Image Config SHA to ordered list of Layer Diff SHAs |
+-------------------------------------------------------------------+
|
+---------------------+---------------------+
| |
v v
+-----------------------+ +-----------------------+
| 3. Image Config JSON | | 4. Layer Diffs |
| Env, Cmd, Entrypt | | tar.gz filesystem |
| rootfs diff_ids | | deltas |
+-----------------------+ +-----------------------+
2. Multi-Architecture Manifest Lists
When you execute docker pull nginx:latest on an Intel x86 server and an Apple Silicon M2 laptop, both commands download an image tagged nginx:latest.
However, the x86 host receives x86_64 binaries, while the ARM host receives aarch64 binaries.
This is made possible by the Manifest List (Image Index).
When a client queries a registry tag, the registry returns a Manifest List mapping architectures to specific digest manifests:
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:1a2b3c4d...",
"platform": { "architecture": "amd64", "os": "linux" }
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"digest": "sha256:9f8e7d6c...",
"platform": { "architecture": "arm64", "os": "linux" }
}
]
}
The Docker engine inspects its local host architecture (amd64), selects matching manifest sha256:1a2b3c4d..., and downloads only the relevant layers.
3. Tarball Layer Diffs and Content Addressing
An image Layer is a compressed tarball (tar.gz) containing filesystem additions, updates, or deletions relative to the parent layer below it.
Layer 3 Tarball: /app/server.js (Updated application code)
Layer 2 Tarball: /usr/bin/node (Node.js runtime environment)
Layer 1 Tarball: /bin/bash, /lib, /etc (Alpine OS rootfs base)
Every layer is identified by a Cryptographic SHA-256 Digest computed over its compressed byte payload.
Content-Addressable Storage (CAS)
Because layers are indexed strictly by SHA-256 digests (sha256:a1b2c3...):
- If 50 different microservices use
node:18-alpineas their base image, the host stores only ONE copy of the Node.js base layers on disk. - Pushing or pulling images checks if a layer digest already exists in storage. If matched, network transmission is skipped entirely.
4. The Whiteout Deletion File Mechanism
What happens when a Dockerfile instruction deletes a file created by an earlier layer?
Consider this Dockerfile:
FROM alpine:3.18
RUN fallocate -l 100M /large_file.dat # Layer 1: Adds 100MB file
RUN rm /large_file.dat # Layer 2: Deletes file
What is the total size of the resulting image?
Engineers often assume Layer 2 reduces image size back to 5MB. It does not. The image size is 105MB!
Why Deletions Do Not Shrink Images
Image layers are immutable. Once Layer 1 is written and hashed, its 100MB payload can never be modified or removed from disk storage.
When Layer 2 executes rm /large_file.dat, the storage driver creates a special marker file inside Layer 2’s tarball called a Whiteout File:
Layer 1 (Read-Only): /large_file.dat (100 MB binary data)
Layer 2 (Read-Only): /.wh.large_file.dat (0 byte whiteout marker)
When OverlayFS mounts Layer 1 and Layer 2 together, the presence of /.wh.large_file.dat in the upper layer hides /large_file.dat from user-space processes.
However, during docker pull, both Layer 1 (100MB) and Layer 2 (whiteout marker) must still be downloaded across the network!
Best Practice: Delete temporary build artifacts (like package caches or tarballs) inside the exact same RUN instruction where they were downloaded!
# GOOD: Deletes temporary files in the SAME layer before commit
RUN apt-get update && apt-get install -y curl \
&& rm -rf /var/lib/apt/lists/*
Summary & Next Steps
Docker images use content-addressable layer architectures for storage efficiency:
- Manifest Lists map target CPU architectures to platform-specific image manifests.
- Image Manifests tie runtime configurations to an ordered array of SHA-256 layer digests.
- Layers are immutable tarball diffs stored using content-addressable digests.
- Whiteout Files (
.wh.filename) mask deleted files in upper layers without removing bytes from lower read-only layers.
In the next article, we inspect Dockerfile Instructions and Layer Mechanics: How Build Caching Works Under the Hood.
References & Further Reading
- Heo, T. (2015). Control Group v2 Documentation (cgroup2.rst). Linux Kernel Docs.
- Gregg, B. (2020). Systems Performance: Enterprise and the Cloud (2nd Edition) — Virtualization & Containers. Addison-Wesley.
- Heo, T. (2015). cgroup v2: The New Unified Control Group Hierarchy. Linux Kernel Summit.
Part 6: Dockerfile Instructions and Layer Mechanics: How Build Caching Works Under the Hood
Continue to Part 6 →