| |

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 :

FieldTypePurpose
schemaVersionintManifest schema version (2)
mediaTypestringMedia type of the manifest
configobjectDescriptor for the image configuration
layersarrayOrdered 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 :

  1. Integrity: Any modification to the content changes the digest, so tampering is detectable.
  2. Deduplication: Identical content has the same digest, so it is stored once.
  3. 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

FieldPurpose
schemaVersionManifest version (2)
mediaTypeManifest media type
configDescriptor for configuration
layersOrdered list of layer descriptors

Descriptor Fields

FieldPurpose
mediaTypeContent type
sizeContent size in bytes
digestSHA256 hash of content

Common Media Types

Media TypeContent
application/vnd.docker.container.image.v1+jsonImage config
application/vnd.docker.image.rootfs.diff.tar.gzipLayer tarball
application/vnd.docker.distribution.manifest.v2+jsonImage manifest
application/vnd.docker.distribution.manifest.list.v2+jsonManifest list

Content Addressable Storage

AspectDetail
Hash algorithmSHA256
Layer digestHash of compressed tarball
Config digestHash of config JSON
Manifest digestHash of manifest JSON
DeduplicationIdentical digests share storage

Layer Operations

CommandPurpose
docker image historyShow layers and sizes
docker inspectShow config and rootfs layers
docker manifest inspectShow manifest list
docker image ls --digestsShow 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

PitfallWhy It HappensFix
Image larger than expectedFiles added then removed in different layersCombine commands in one RUN
Layer not sharedDifferent base image or modified baseUse identical base image
Secrets exposedCopied into imageUse build secrets or runtime env
Pull failsRegistry doesn’t support manifest formatCheck Accept header
Platform mismatchManifest list resolves to wrong platformSpecify --platform
Layer cache missInstruction order wrongPut 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

ItemValue
Image manifestJSON describing config and layers
Manifest schemaVersion 2
Manifest listMulti-platform “fat manifest”
Config descriptorMedia type, size, digest
Layer descriptorMedia type, size, digest
Content addressingSHA256 digest of content
Layer digestHash of compressed tarball
Layer orderBase layer first
Union filesystemStacks layers into one view
Writable layerAdded on top when container runs
Copy-on-writeFile copied to writable layer on modification
Base imageFoundation 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!