Docker 11 🐳 Docker Images Fundamentals: Image Manifests, Content Addressable Storage, and Layers
A Docker image is not a single file. It is a manifest that references a configuration and an ordered list of layers, and each layer is stored as a content-addressed blob. This architecture is what makes images distributable, shareable, and verifiable. When you pull an image, Docker does not download a monolithic tarball; it fetches the manifest, reads the layer digests, and downloads only the layers that are missing locally. When you push an image, the same manifest and layer blobs are uploaded, and the registry stores each blob under its digest. The result is an image format that is both efficient in storage and secure in transit.
The shift to content-addressable storage happened in Docker Engine 1.10. Before that version, images and layers were identified by randomly assigned UUIDs, which meant two identical layers from different builds were treated as distinct and stored twice . The 1.10 release replaced UUIDs with cryptographic hashes of the content, so identical layers are automatically deduplicated and any modification to a layer changes its digest . This change made layer sharing across images reliable and gave the distribution system a built-in integrity check.
This chapter covers the image manifest and its schema, the manifest list for multi-architecture images, content-addressable storage and digests, the layered filesystem and how layers combine into a container, and the commands that inspect and manage image layers.
Key point: A Docker image is a manifest that references a configuration and an ordered list of layers. Each layer is a content-addressed blob identified by its SHA256 digest. Layers are shared across images, so identical layers are stored once. The manifest list (fat manifest) allows a single image name to resolve to different manifests for different platforms.
Why image manifests and content-addressable storage exist
The layer-sharing problem. Two images built from the same base image share the base layers. Without a content-addressed model, the registry and the local Docker daemon would have no reliable way to know that the layers are identical. Content addressing makes sharing automatic: if the digest matches, the layer is already present .
The integrity problem. When an image is pulled from a registry, the client needs to verify that the bytes it received are the bytes that were published. Each layer’s digest is a cryptographic hash of its content, and the manifest references the digest. If the layer is tampered with, the digest no longer matches, and the client rejects it .
The multi-platform problem. A single image name like ubuntu:latest must work on Linux AMD64, Linux ARM64, and other platforms. The manifest list (also called a fat manifest) solves this: it is a top-level manifest that references platform-specific image manifests. The client sends its platform in the Accept header, and the registry returns the appropriate manifest .
The build-efficiency problem. Dockerfile instructions create layers, and the order of layers matters for caching. Because layers are content-addressed, a layer that has not changed does not need to be rebuilt or re-downloaded. Changing a late instruction in the Dockerfile only invalidates the layers after that point .
The deduplication problem. Multiple images that share a base layer store that layer once. On the host, the storage driver links or copies the layer content into each image’s filesystem, but the underlying blob exists once in the content-addressable store .
a. The image manifest
An image manifest is a JSON document that describes a single image for a specific platform. It references a configuration object and an ordered list of layers .
The manifest schema version 2 has these fields :
| Field | Type | Purpose |
|---|---|---|
schemaVersion | int | Manifest schema version (2) |
mediaType | string | Media type of the manifest |
config | object | Descriptor for the image configuration |
layers | array | Ordered list of layer descriptors |
The config object is a descriptor: it contains a mediaType (application/vnd.docker.container.image.v1+json), a size, and a digest (SHA256 hash) . The configuration JSON contains the image’s runtime settings, the root filesystem layer list, and the history of build steps.
The layers array is ordered from the base layer first. Each layer is also a descriptor with a mediaType (typically application/vnd.docker.image.rootfs.diff.tar.gzip), a size, and a digest . The layer digest in the manifest is the digest of the compressed tarball as stored in the registry.
A minimal image manifest looks like this:
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 7023,
"digest": "sha256:123456abcdef..."
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 32654,
"digest": "sha256:abcdef123456..."
}
]
}
The manifest is retrieved from the registry with a GET /v2/<name>/manifests/<reference> request, where the reference is a tag or digest . The client sends an Accept header to indicate which manifest format it supports .
b. The manifest list (fat manifest)
A manifest list is a top-level manifest that references multiple platform-specific image manifests. It is used for multi-architecture images .
When a client requests ubuntu:latest, the registry returns the manifest list if the client supports it. The manifest list contains entries for each platform, each with a digest pointing to the platform-specific manifest. The client selects the entry matching its operating system and architecture, then fetches that manifest .
The manifest list media type is application/vnd.docker.distribution.manifest.list.v2+json . The OCI equivalent is application/vnd.oci.image.index.v1+json .
The docker manifest command manages manifest lists. It can inspect a manifest list, annotate entries, and create a list from multiple platform-specific images .
c. Content-addressable storage and digests
Content-addressable storage means that a piece of content is addressed by the hash of its content, not by a name or UUID. Docker uses SHA256 digests for layers, configurations, and manifests .
The digest of a layer is computed from the compressed tarball as stored in the registry. The digest of the configuration is computed from the configuration JSON. The digest of the manifest is computed from the manifest JSON. The manifest is referenced by a tag (like latest) or by its digest .
The benefit of content addressing is threefold :
- Integrity: Any modification to the content changes the digest, so tampering is detectable.
- Deduplication: Identical content has the same digest, so it is stored once.
- Sharing: Different images that contain the same layer can reference the same blob.
On the Docker host, the content-addressable data is stored under /var/lib/docker and managed by the storage driver. The storage driver extracts each layer into its own directory, and the union filesystem stacks them when a container runs .
The docker inspect command shows the digests and layer information for an image. The docker image history command shows the layers that make up an image, along with the commands that created them .
d. Image layers and the union filesystem
An image is a stack of layers. Each layer contains a set of filesystem changes: additions, modifications, and deletions . The first layer is the base image (like Ubuntu), and subsequent layers add or modify files.
When a container runs, the storage driver creates a union filesystem where the image layers are stacked on top of each other, creating a unified view . A thin writable layer is added on top for the container’s own changes .
The union filesystem is what allows the container to see a complete filesystem even though the layers are stored separately. When the container reads a file, the union filesystem searches from the topmost layer downward until it finds the file. When the container modifies a file, the storage driver performs a copy-on-write operation: it copies the file from the read-only layer into the writable layer, and the container’s changes are made to that copy .
The layers are immutable. Once a layer is created, it cannot be changed. If a Dockerfile instruction modifies a file that exists in a previous layer, the modification is stored as a new layer, and the original file in the lower layer is obscured . This is why deleting a file in a later layer does not reduce the image size: the file still exists in the lower layer, and the deletion is recorded as a whiteout file in the upper layer .
The docker image history command shows the layers of an image, the commands that created them, and the size of each layer . The docker history output is useful for identifying which instruction contributed the most to the image size.
e. Base images and parent images
A base image is a foundation for building other images. It usually contains a minimal operating system and package manager . A parent image is the image referenced in the FROM instruction of a Dockerfile .
The distinction is subtle: a base image is a starting point from scratch or from a minimal OS, while a parent image is whatever the current image is built on top of. In practice, the terms are often used interchangeably .
Common base images include ubuntu, alpine, debian, and scratch. The scratch image is empty; an image based on scratch contains only what the Dockerfile adds. This is used for statically compiled binaries that need no runtime .
The choice of base image affects the image size, the available tools, and the security surface. Alpine is popular for its small size, but it uses musl libc instead of glibc, which can cause compatibility issues with binaries that expect glibc. Debian and Ubuntu are larger but more compatible.
Complete Example Session
# ============================================
# PART 1: PULL AN IMAGE AND VIEW LAYERS
# ============================================
docker pull nginx:latest
docker image history nginx:latest
# Shows each layer, the command that created it, and its size
# ============================================
# PART 2: INSPECT THE IMAGE CONFIGURATION
# ============================================
docker inspect nginx:latest
# Shows the image ID, config, rootfs layers, and history
# ============================================
# PART 3: VIEW THE MANIFEST LIST
# ============================================
docker manifest inspect nginx:latest
# Shows the manifest list with platform-specific manifests
# ============================================
# PART 4: VIEW LAYER DIGESTS
# ============================================
docker inspect --format '{{json .RootFS.Layers}}' nginx:latest
# ["sha256:...", "sha256:...", ...]
# ============================================
# PART 5: RUN A CONTAINER AND SEE UNION FS
# ============================================
docker run -d --name web nginx:latest
docker exec web ls /
# The container sees a merged view of all layers
# ============================================
# PART 6: MODIFY A FILE (COPY-ON-WRITE)
# ============================================
docker exec web sh -c "echo 'modified' > /usr/share/nginx/html/index.html"
docker exec web cat /usr/share/nginx/html/index.html
# The modification is in the writable layer
# ============================================
# PART 7: VERIFY LAYERS ARE UNCHANGED
# ============================================
docker run --rm nginx:latest cat /usr/share/nginx/html/index.html
# Shows the original content; the image layers are unchanged
# ============================================
# PART 8: BUILD AN IMAGE AND VIEW LAYERS
# ============================================
cat > Dockerfile << 'EOF'
FROM alpine:3.19
RUN apk add --no-cache curl
COPY app.sh /app.sh
CMD ["/app.sh"]
EOF
docker build -t my-app .
docker image history my-app
# ============================================
# PART 9: SHARE A LAYER BETWEEN IMAGES
# ============================================
docker pull alpine:3.18
docker pull alpine:3.19
# The base layers may be shared if the digests match
# ============================================
# PART 10: INSPECT THE MANIFEST VIA API
# ============================================
curl -s -H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
https://registry-1.docker.io/v2/library/nginx/manifests/latest | jq .
These ten parts cover pulling an image, viewing layer history, inspecting the configuration, viewing the manifest list, running a container to see the union filesystem, modifying a file (copy-on-write), verifying the original layers are unchanged, building an image, sharing layers, and inspecting the manifest via the registry API.
Quick Reference
Image Manifest Fields
| Field | Purpose |
|---|---|
schemaVersion | Manifest version (2) |
mediaType | Manifest media type |
config | Descriptor for configuration |
layers | Ordered list of layer descriptors |
Descriptor Fields
| Field | Purpose |
|---|---|
mediaType | Content type |
size | Content size in bytes |
digest | SHA256 hash of content |
Common Media Types
| Media Type | Content |
|---|---|
application/vnd.docker.container.image.v1+json | Image config |
application/vnd.docker.image.rootfs.diff.tar.gzip | Layer tarball |
application/vnd.docker.distribution.manifest.v2+json | Image manifest |
application/vnd.docker.distribution.manifest.list.v2+json | Manifest list |
Content Addressable Storage
| Aspect | Detail |
|---|---|
| Hash algorithm | SHA256 |
| Layer digest | Hash of compressed tarball |
| Config digest | Hash of config JSON |
| Manifest digest | Hash of manifest JSON |
| Deduplication | Identical digests share storage |
Layer Operations
| Command | Purpose |
|---|---|
docker image history | Show layers and sizes |
docker inspect | Show config and rootfs layers |
docker manifest inspect | Show manifest list |
docker image ls --digests | Show image digests |
Best Practices
✅ Do This:
# Use a specific base image tag
FROM alpine:3.19
# Combine commands to reduce layers
RUN apk add --no-cache curl && \
apk add --no-cache jq
# Use multi-stage builds to reduce final image size
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o /app
FROM alpine:3.19
COPY --from=builder /app /app
CMD ["/app"]
❌ Don’t Do This:
# Use :latest for production
FROM alpine:latest # ❌ unpredictable
# Install and remove in separate layers
RUN apk add --no-cache build-base # ❌
RUN apk del build-base # ❌ file still in lower layer
# Include secrets in the image
COPY .env /app/.env # ❌ secrets in layer history
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Image larger than expected | Files added then removed in different layers | Combine commands in one RUN |
| Layer not shared | Different base image or modified base | Use identical base image |
| Secrets exposed | Copied into image | Use build secrets or runtime env |
| Pull fails | Registry doesn’t support manifest format | Check Accept header |
| Platform mismatch | Manifest list resolves to wrong platform | Specify --platform |
| Layer cache miss | Instruction order wrong | Put stable instructions first |
Real-World Examples
1. View Image Layers
docker image history nginx:latest
2. View Manifest List
docker manifest inspect nginx:latest
3. Inspect RootFS Layers
docker inspect --format '{{json .RootFS.Layers}}' nginx:latest
4. Multi-Stage Build
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o /app
FROM alpine:3.19
COPY --from=builder /app /app
CMD ["/app"]
5. Combine Commands to Reduce Layers
RUN apt-get update && \
apt-get install -y curl && \
rm -rf /var/lib/apt/lists/*
6. Use Specific Base Tag
FROM node:20.11-alpine
7. Verify Layer Digest
docker inspect --format '{{.RepoDigests}}' nginx:latest
8. Pull Specific Platform
docker pull --platform linux/arm64 nginx:latest
9. Build and Push with Digest
docker build -t myapp .
docker push myapp:latest
docker inspect --format '{{.RepoDigests}}' myapp:latest
10. Inspect Manifest via API
curl -s -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
https://registry-1.docker.io/v2/library/alpine/manifests/latest | jq .
Visual
Image Manifest Structure
┌──────────────────────────────────────────────────────────────┐
│ MANIFEST (application/vnd.docker.distribution.manifest.v2+json)│
│ │
│ { │
│ "schemaVersion": 2, │
│ "mediaType": "...", │
│ "config": { │
│ "mediaType": "...container.image.v1+json", │
│ "size": 7023, │
│ "digest": "sha256:config..." │
│ }, │
│ "layers": [ │
│ {"digest": "sha256:layer1...", "size": 32654}, │
│ {"digest": "sha256:layer2...", "size": 16724}, │
│ {"digest": "sha256:layer3...", "size": 73109} │
│ ] │
│ } │
└──────────────────────────────────────────────────────────────┘
Manifest List (Fat Manifest)
┌──────────────────────────────────────────────────────────────┐
│ MANIFEST LIST │
│ ├── linux/amd64 → manifest digest A │
│ ├── linux/arm64 → manifest digest B │
│ ├── linux/arm/v7 → manifest digest C │
│ └── windows/amd64 → manifest digest D │
│ │
│ Client sends Accept: manifest.list.v2+json │
│ Registry returns the list; client selects its platform │
└──────────────────────────────────────────────────────────────┘
Content Addressable Storage
┌──────────────────────────────────────────────────────────────┐
│ REGISTRY │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ sha256:abc123... (layer blob) │ │
│ │ sha256:def456... (layer blob) │ │
│ │ sha256:ghi789... (config blob) │ │
│ │ sha256:jkl012... (manifest blob) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Each blob is addressed by its digest. │
│ Identical content → identical digest → stored once. │
└──────────────────────────────────────────────────────────────┘
Layer Stack and Union Filesystem
┌──────────────────────────────────────────────────────────────┐
│ CONTAINER VIEW (union filesystem) │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Writable Layer (container-specific changes) │ │
│ ├────────────────────────────────────────────────────────┤ │
│ │ Layer 3: RUN apk add curl │ │
│ ├────────────────────────────────────────────────────────┤ │
│ │ Layer 2: COPY app.sh /app.sh │ │
│ ├────────────────────────────────────────────────────────┤ │
│ │ Layer 1: FROM alpine:3.19 │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ Read-only layers are shared across all containers │
│ from the same image. The writable layer is unique. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Image manifest | JSON describing config and layers |
| Manifest schema | Version 2 |
| Manifest list | Multi-platform “fat manifest” |
| Config descriptor | Media type, size, digest |
| Layer descriptor | Media type, size, digest |
| Content addressing | SHA256 digest of content |
| Layer digest | Hash of compressed tarball |
| Layer order | Base layer first |
| Union filesystem | Stacks layers into one view |
| Writable layer | Added on top when container runs |
| Copy-on-write | File copied to writable layer on modification |
| Base image | Foundation for building images |
Key takeaways:
- An image manifest references a configuration and an ordered list of layers. Each reference is a descriptor with a media type, size, and SHA256 digest. The manifest is the entry point for pulling an image .
- A manifest list (fat manifest) enables multi-architecture images. A single image name resolves to different manifests for different platforms. The client selects the manifest matching its OS and architecture .
- Content-addressable storage uses SHA256 digests as identifiers. Identical content has the same digest and is stored once. Any modification changes the digest, providing integrity verification .
- Layers are immutable and shared across images. Each layer contains filesystem changes. Images that share a base layer share the base layer’s blob in the registry and on the host .
- The union filesystem stacks layers into a unified view. When a container runs, the storage driver creates a merged view of all layers plus a thin writable layer for the container’s changes .
- Copy-on-write modifies files without changing the image. When a container modifies a file, the storage driver copies it to the writable layer, leaving the read-only layer unchanged .
- The Dockerfile creates layers. Each instruction that modifies the filesystem creates a layer. The order of instructions affects caching and the final image size .
- Never include secrets in an image. Layers are immutable, so a secret copied into a layer is permanently part of the image history, even if deleted in a later layer .
Remember: A Docker image is a manifest that references a configuration and a stack of layers, each stored as a content-addressed blob. The manifest is the contract between the image and the registry; the digests are the integrity mechanism; the layers are the filesystem. Understanding this architecture explains why images are shareable, why identical layers are stored once, why modifying a file in a container does not change the image, and why secrets must never be baked into a layer. The docker image history command shows the layers and their sizes; docker inspect shows the configuration and the rootfs layers; docker manifest inspect shows the platform-specific manifests. These commands are the tools for inspecting the image structure and diagnosing problems that stem from how the image was built.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!