The problem
You have been using three different "IDs" for an image - the IMAGE ID from docker images, the sha256: digest from a pull or push, and the layer IDs in docker inspect - and they never match each other. You have also pulled images where only some layers downloaded. Both make sense once you see what an image is made of on the registry and on disk.
What you need to know already: tags vs digests (10.5), layers and whiteouts (10.10), docker save and tar (10.42), jq (Ch 7). A hash (sha256 here) is a fixed-length fingerprint computed from content: change one byte and the hash changes completely.
Three kinds of object
An image in a registry is not one file. It is a few files, each called a blob (just "a chunk of bytes, named by its hash"):
index (optional) a list of manifests, one per platform - multi-arch images
manifest the list of layer blobs and the config blob, each by digest
config JSON: architecture, os, the runtime config (Env, Entrypoint, Cmd,
User, WorkingDir...), the history, and rootfs.diff_ids
layers tar archives (compressed in the registry), one per filesystem step
- layers - the files, one tar per filesystem step (COPY, RUN...)
- config - everything that is not files: settings from ENV, ENTRYPOINT, CMD, USER, WORKDIR, plus the build history
- manifest - the table of contents: "this config, these layers, in this order"
- index - only for multi-arch images: "for arm64 use this manifest, for amd64 that one"
Everything references everything else by sha256 digest, which is why the whole structure is tamper-evident (any change is detectable): change one byte in one layer and its digest changes, so the manifest changes, so the manifest's digest - the thing you pinned - no longer matches.
Which ID is which:
- The image digest (
name@sha256:..., whatdocker pushprints) is the digest of the manifest (or of the index, for multi-arch). - The IMAGE ID in
docker imagesis the digest of the config. - The
RootFS.Layersindocker inspectare diff IDs: digests of the uncompressed layer tars. The registry stores compressed blobs with different digests; that is why the IDs you see indocker pulloutput do not matchdocker inspect.
Looking at it on disk
docker save -o file.tar <image> writes the image in the standard OCI layout (OCI = the Open Container Initiative, which standardises the format):
$ docker save -o orders.tar orders:tiny && mkdir o && tar -xf orders.tar -C o
$ ls o
blobs index.json manifest.json oci-layout repositories
$ cat o/manifest.json
[{"Config":"blobs/sha256/1f3e...","RepoTags":["orders:tiny"],"Layers":["blobs/sha256/da20...","blobs/sha256/5c1e...","blobs/sha256/77ab..."]}]
blobs/sha256/- every blob, named by its hashmanifest.json- Docker's own summary: the config file and the layers in order, oldest first
The config blob is plain JSON, so jq reads it. This command picks the config path out of manifest.json ($(...), Ch 6) and prints a few fields:
$ jq '{architecture, os, user: .config.User, entrypoint: .config.Entrypoint, history: [.history[].created_by]}' o/$(jq -r '.[0].Config' o/manifest.json)
{
"architecture": "arm64",
"os": "linux",
"user": "app",
"entrypoint": ["java","-jar","app.jar"],
"history": [
"# debian.sh --arch 'arm64' out/ 'bookworm' '@1726444800'",
"CMD [\"bash\"]",
"ENV JAVA_HOME=/opt/java/openjdk",
...
]
}
That config - history included - travels with the image to every registry and server. It is where docker history gets its data, and where a leaked ARG value lives.
Each layer blob is a tar of only what that step changed, including whiteouts for deletions. tar -tvf lists it (-t list, -v with owner, size and date, -f this file); .[0].Layers[-1] in jq is the last (newest) layer:
$ tar -tvf o/$(jq -r '.[0].Layers[-1]' o/manifest.json)
-rw-r--r-- 999/999 22012344 2026-09-23 10:00 app/app.jar
Why this matters day to day
- Pulls are per layer. A server that already has the base layers downloads only your top layers. Stable bases and small top layers make deploys fast - the reason for the layered-jar mission (10.27).
- Registries deduplicate by digest. Ten services on the same base store the base once.
- Signatures attach to digests. Tools like cosign sign an image (attach a cryptographic proof of who built it, like the SSH keys in Ch 1) by signing its manifest digest, so a server can refuse images nobody signed.
- A digest pins everything: every layer and the config - including the entrypoint and user. Pinning
@sha256:is stronger than it looks.
What you can now do
- Say which object each ID belongs to: IMAGE ID = config, digest = manifest, RootFS layers = uncompressed layer tars
- Unpack an image with
docker save+tarand read its config withjq - Explain why a pull downloads only the layers a server does not have yet