Step 1 of 5 · Reading · ~3 min
Read
Filesystems & Images
OCI Image Format
"Image" sounds like a file. It is not — an image is a small graph of blobs that all reference each other by the hash of their own contents, and understanding that graph explains image IDs, deduplication, signing and docker pull in one go.
Three kinds of blob
- A manifest: JSON naming one config blob and an ordered list of layer blobs.
- A config: JSON with the architecture, OS, environment, entrypoint, the layer
diffIDs and the build history. - The layers themselves: tar archives, usually gzipped.
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:...",
"size": 7023
},
"layers": [
{ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:...", "size": 32654 }
]
}
Every reference in that document is a digest, never a name or a path. That is what "content-addressable" buys: a registry can verify what it served, a client can verify what it received, and two parties can agree on an image without trusting each other's naming.
The cascade
Change one byte of the config — a different ENV, a different entrypoint — and the consequences run upward, mechanically:
config bytes change
-> config digest changes (the image ID is the config's digest)
-> the manifest's config.digest field changes
-> the manifest's own digest changes
-> pull-by-digest for the old image still resolves, to the old image
The layers did not change and are not re-uploaded. This is also why docker images can show two IDs for what looks like the same build, and why a tag is not an identity: tags are mutable pointers, digests are not.
digest against diffID
The one genuine subtlety in the format, and a routine interview question:
digest— SHA-256 of the compressed blob. It is what the registry stores and what travels on the wire, so the manifest uses it.diffID— SHA-256 of the uncompressed tar. It is what the runtime stacks, so the config uses it.
Two registries may gzip the same layer to different bytes and so publish different digests, while the runtime sees one diffID and stacks one layer. Separating the two lets transfer and stacking each be content-addressed on the bytes that matter to them.
Why the exercise is a simulator
Nothing here is privileged — the exercise is a simulation only in that it drops gzip, so digest and diffID coincide. Everything else is the real construction: hash the content, record the size, keep the layers in order, and canonicalise the config before hashing it.
Your exercise: Build an OCI Manifest
| A builder does | Your simulator does |
|---|---|
| write a layer tar, hash it, record its size | LAYER <id> <content> — emit sha256:<hex> <size> |
| accumulate config fields | CONFIG <key>=<value> ... |
| append a layer descriptor, in order | ADDLAYER <id> — or ERR no such layer |
| serialise the manifest | MANIFEST |
| ask the registry whether a blob is present | PULL <digest> |
Two details decide whether your output matches. Canonicalise before you hash: the config digest is the SHA-256 of key=value pairs joined with ; in lexicographic key order, so CONFIG os=linux arch=amd64 and CONFIG arch=amd64 os=linux must produce the same digest — that is the whole point of a canonical form. And size is bytes, not characters: hash and measure the encoded content, so a multi-byte character counts once for the hash and more than once for the size.
MANIFEST prints the two fixed lines, the config line, then every layer: line in ADDLAYER order, and only then every diffID: line in the same order — two passes over the same list, not one interleaved pass.
Reference: https://github.com/opencontainers/image-spec/blob/main/manifest.md
Discussion
Ask a question, share an insight, or help someone who’s stuck.
Sign in to post a comment or reply.
Loading…