containerd doesn’t just use the OCI Image Specification; it’s built from the ground up to be a compliant implementation, which means it understands and can manipulate container images in precisely the way the spec defines.
Let’s see this in action. Imagine you have a simple OCI image layout, which is just a directory containing index.json and a blobs directory.
./my-oci-layout/
├── index.json
└── blobs/
└── sha256/
└── ...
index.json points to a specific manifest, which in turn describes the image’s layers and configuration. containerd can directly import this.
$ ctr image import --platform linux/amd64 ./my-oci-layout
...
sha256:abcdef123456...
This command tells containerd to read the index.json from ./my-oci-layout, validate it against the OCI Image Specification, and store the image’s layers and configuration content addressably in its own internal blob store. The output is the digest of the manifest, the primary identifier for this image within containerd.
The OCI Image Specification is essentially a contract for how image components are represented and referenced. It defines three core types of objects:
- Image Index: A JSON file that can point to one or more manifests, typically for different architectures or operating systems. This is the top-level entry point.
- Image Manifest: A JSON file describing the image’s layers and its configuration. It includes media type, digests, and sizes for each component.
- Image Configuration: A JSON file containing the runtime configuration for the container, such as command, entrypoint, environment variables, and the filesystem layer digest.
containerd’s internal remotes and content packages are the workhorses here. The remotes package handles fetching and pushing images from/to registries, translating registry protocols into OCI spec objects. The content package is the local storage for these blobs, keyed by their SHA256 digests. When ctr image import runs, containerd downloads the index.json (if present), then the appropriate manifest.json, then the config.json, and finally all the layer blobs. It verifies the digest of each downloaded blob against the references in the manifest and stores them in its content store.
The beauty of this is that containerd treats these blobs as immutable, content-addressable data. Once a blob (a layer, or the config) is imported, its digest is its permanent identifier. If you import the exact same image again, containerd won’t re-download anything; it’ll just see that the blobs already exist and update its internal references. This is fundamental to efficient image management.
Consider the config.json. This isn’t just metadata; it’s the actual blueprint for how a container should run. containerd parses this, extracts the Cmd, Entrypoint, Env, WorkingDir, etc., and uses them when it’s time to create a container from that image. The rootfs field within the config is particularly interesting; it lists the digests of the filesystem layers that, when stacked in the specified order, form the container’s root filesystem.
The OCI Image Specification is quite detailed about media types. For instance, a layer blob typically has the media type application/vnd.oci.image.layer.v1.tar+gzip, and the configuration blob is application/vnd.oci.image.config.v1+json. containerd validates these media types to ensure it’s dealing with correctly formatted data.
This adherence means containerd can seamlessly interoperate with any OCI-compliant registry. It’s not tied to a specific vendor’s format.
When containerd fetches an image, it first requests the index.json (or directly the manifest if no index is involved for a specific platform). It then uses the digests and media types within the manifest to fetch the configuration and all layer blobs. Each fetched blob’s SHA256 digest is computed and compared against the digest specified in the manifest. If they match, the blob is stored. If not, the import fails. This digest verification is the core security and integrity mechanism.
If you want to see what’s inside containerd’s blob store for an imported image, you can poke around its data directory (often /var/lib/containerd/content/) but it’s not designed for human readability – it’s all keyed by digests. However, you can use ctr to inspect the image metadata.
$ ctr images list
REF ...
docker.io/library/ubuntu:latest@sha256:e10f250795900522a58f3622f750765521726c1559e59b216012073689c6460a
...
$ ctr image inspect docker.io/library/ubuntu:latest@sha256:e10f250795900522a58f3622f750765521726c1559e59b216012073689c6460a
...
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:750765521726c1559e59b216012073689c6460a476626395c2f361380f6f361",
"size": 2512
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:e69867a2315a447800834297433c6322e384b7881b871b0f1380942406c82827",
"size": 32097869
},
...
This output directly reflects the structure defined by the OCI Image Specification: a manifest (the outermost JSON object you’re inspecting) referencing a config blob and multiple layer blobs, all identified by their digests and types.
The OCI Image Specification also defines the oci-archive format, which is essentially a tarball containing the index.json, manifest.json, config.json, and layer blobs. containerd can import and export images in this format as well, making it easy to move images between systems without a registry.
The most surprising aspect of containerd’s OCI implementation is how it handles image history. The OCI spec’s configuration object includes a history field, which is an array of objects, each representing a command that created a layer. containerd doesn’t actively use this history for runtime execution, but it faithfully stores and exposes it as part of the image metadata, preserving the lineage information as defined by the spec.
The next concept you’ll run into is how containerd uses these OCI images to run containers, which involves the OCI Runtime Specification.