skip to content

Describe the objects that make up an OCI container image — what a manifest, an image config and a layer blob each contain, and how digests tie them together.

level: middleimportance: must knowfreq 50%

answer

  1. descriptor = mediaType + digest + size
  2. manifest → config + ordered layers
  3. config = Entrypoint/Env/User + diff_ids + history
  4. image ID = config digest; pin = manifest digest
  5. whiteout .wh.<name> for deletes

basics

~20 s

An image is a graph of digest-named blobs. The manifest lists a config descriptor and ordered layer descriptors. The config JSON holds runtime defaults (Entrypoint, Cmd, Env, User), build history and rootfs.diff_ids. Each layer blob is a tar of filesystem changes. The image's own ID is the digest of its config.

solid answer

~50 s

Everything is **content-addressed**: each object's name is the SHA-256 digest of its bytes. - **Manifest** — the entry point for one platform. It contains a descriptor (`mediaType`, `digest`, `size`) for the **config** blob and an ordered array of descriptors for the **layer** blobs. - **Image config** — a JSON blob with the defaults the build baked in (`Env`, `Entrypoint`, `Cmd`, `User`, `WorkingDir`, `Labels`), the `history` entries, and `rootfs.diff_ids`, the digests of the *uncompressed* layer tars in apply order. - **Layer blobs** — tar archives of filesystem changes, normally gzip or zstd compressed. Deletions are encoded as `.wh.<name>` whiteout entries. Layer descriptors carry the *compressed* digest; `diff_ids` carry the uncompressed one. What people call the "image ID" is the digest of the config blob; the `repo@sha256:…` you pin in production is the digest of the manifest or index. Because layers are named by content, two images sharing a base share the blob in the registry and on disk.

code

json · 15 lines
json
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "digest": "sha256:c1f2...",
    "size": 1471
  },
  "layers": [
    { "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:aa11...", "size": 31298112 },
    { "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:bb22...", "size": 402 }
  ]
}

go deeper

for a junior

Know that an image is layers plus metadata, that layers are read-only and stacked in order, and that digests identify content.

for a middle

Name the manifest / config / layer objects, what each holds, and which digest is the image ID versus the pull digest.

for a senior

Reason from the layout to real consequences: layer sharing and cache behaviour, secrets surviving in earlier layers, digest pinning for reproducible and verifiable deploys.

for a principal

Use the format as policy leverage — digest-pinned promotion between registries, signing the manifest digest, base-image sharing and storage cost across a fleet.

## Content addressing is the organizing idea An OCI image is not a file — it is a small directed graph of **blobs**, each named by the SHA-256 digest of its own bytes (`sha256:9f86d0…`). Rename nothing, trust the hash: if two blobs have the same digest they are byte-identical, so a registry stores them once, a host downloads them once, and any tampering changes the name. Every reference in the graph is a **descriptor** — `mediaType`, `digest`, `size`, plus optional `annotations` and `urls`. ## The manifest The manifest is the per-platform root of the graph: ``` mediaType: application/vnd.oci.image.manifest.v1+json config: { mediaType: …config.v1+json, digest: sha256:…, size: … } layers: [ { mediaType: …layer.v1.tar+gzip, digest: sha256:…, size: … }, … ] ``` The `layers` array is **ordered**: layer 0 is applied first, later layers are applied on top. That order is semantically meaningful — a file created in layer 1 and deleted in layer 3 does not appear in the final rootfs, but its bytes are still shipped in layer 1. This is why a secret added and later `rm`-ed in a Dockerfile is still recoverable from the image. Above the manifest there may be an **image index** listing several manifests with `platform` fields, which is how one tag serves `linux/amd64` and `linux/arm64`. ## The image config The config is itself just another blob, but it is the one that describes *behaviour*: - `config` — the defaults baked in at build time: `Env`, `Entrypoint`, `Cmd`, `User`, `WorkingDir`, `ExposedPorts`, `Volumes`, `Labels`, `StopSignal`. - `rootfs.diff_ids` — the ordered digests of each layer's **uncompressed** tar. These identify filesystem content independently of how it was compressed. - `history` — one entry per build step, with `created_by` (the Dockerfile instruction) and `empty_layer: true` for metadata-only steps such as `ENV` or `LABEL`. This is what `docker history` renders. - `architecture`, `os`, `created`. The **image ID** you see in `docker images` is the digest of this config blob. That is why changing only a `LABEL` — no filesystem change at all — still produces a new image ID: the config bytes changed. ## The layers Each layer blob is a tar archive of the filesystem *changes* introduced by one build step: files added or modified verbatim, deletions encoded as zero-length `.wh.<filename>` entries (and `.wh..wh..opq` to opaque out a whole directory). The manifest's layer descriptor names the compressed blob; the config's `diff_id` names the uncompressed tar. A runtime that unpacks layers verifies the compressed digest on download and the diff ID after decompression. At runtime the layers are stacked read-only by a storage driver (typically overlayfs) with a thin writable layer on top. That stacking mechanism is storage-driver territory; the spec's contribution is simply the ordered, digest-named list. ## Digests you will be asked to distinguish Three different digests get conflated constantly: 1. **Layer digest** — of the compressed blob; what the registry serves at `/v2/<name>/blobs/<digest>`. 2. **diff_id** — of the uncompressed layer tar; lives in the config's `rootfs`. 3. **Config digest** — of the config blob; this is the *image ID*. And separately, the **manifest digest** (or index digest) is what a registry returns in the `Docker-Content-Digest` header and what you pin with `myrepo/app@sha256:…`. Pinning by manifest digest is the reproducible reference; a tag is a mutable pointer that can be moved to a different manifest at any time. ## Practical consequences - **Sharing and caching.** Two images built `FROM` the same base reference the same layer digests, so the base is stored once per registry and pulled once per host. Reordering Dockerfile instructions changes later layer content and destroys that sharing. - **Nothing is ever really deleted.** Because layers are additive, deleting a file in a later layer only masks it. Secrets must never enter any layer. - **Immutability.** Verifying an image means verifying digests down the graph — index → manifest → config and layers. Signing (cosign and friends) signs the manifest digest, which transitively covers everything. - **Reproducible deploys.** Deploy by digest, not by tag, when you need to know exactly which bytes are running.

  • You deleted a credentials file in a later Dockerfile layer. Is it gone from the image?
    No. Layers are additive: the later layer only records a whiteout entry that masks the file in the merged view. The original bytes still ship inside the earlier layer blob and anyone who pulls the image can extract them. The fix is to never let the secret into a layer — use a build secret mount or a multi-stage build that leaves the file behind.
  • Why does a tag give you a weaker guarantee than a digest reference?
    A tag is a mutable pointer in the registry: the same tag can be repushed to point at a completely different manifest, so two pulls a week apart can yield different bytes. A `repo@sha256:…` reference names the manifest content itself, so the registry either returns exactly those bytes or fails. Production deploys and signature verification should pin digests.

saying these in an interview costs you the question

  • Calling an image "a tarball" or one file rather than a manifest plus separately-stored blobs.
  • Believing a file deleted in a later layer is removed from the image.
  • Confusing the image ID (config digest) with the pull digest (manifest digest).
  • Assuming layer order does not matter, or that layers are a set rather than an ordered list.
  • Thinking each Dockerfile instruction always produces a layer — ENV, LABEL and similar are metadata-only history entries.

context