KubernetesI · Container and Distributed Systems FoundationsContainer and distributed systems foundations
Containers, images and OCI — what the kubelet actually pulls
What you'll learn
- Describe the OCI image spec: manifest, config, layers, digest
- Explain how registries store and serve images and how kubelet authenticates to them
- Reason about image identity vs tag identity and why digests are the production anchor
- Identify the failure modes around image pulls, layer caching, and registry availability
Prerequisites
Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16
Kubernetes is an orchestrator of containers. It does not run containers itself — kubelet delegates to a runtime (containerd by default) via the Container Runtime Interface. The runtime’s job is to pull, unpack, and start an image that conforms to the OCI Image Specification. This lesson covers the pieces the runtime hands to the kernel and the ways the abstraction can break in production.
What an OCI image is
The OCI Image Spec defines an image as three things:
- A manifest (
index.jsonfor multi-platform,manifest.jsonfor a single-platform image) listing the layers and config - A config (
<digest>.json) describing the image’s runtime parameters: entrypoint, environment, working directory, exposed ports, volumes, OS, architecture - Layers (
<digest>tarballs) that are overlaid to produce the root filesystem
The whole image is content-addressed by digest (a SHA-256 over each component). The image has no inherent name — it has a manifest digest, and tags are mutable pointers to a digest.
flowchart TB
R[Registry] -->|pull by tag| M[Manifest<br/>sha256:abc123]
M --> C[Config<br/>sha256:def456]
M --> L1[Layer 0<br/>sha256:ghi789]
M --> L2[Layer 1<br/>sha256:jkl012]
M --> L3[Layer N<br/>sha256:mno345]
C --> FS[Unpacked root filesystem]
L1 --> FS
L2 --> FS
L3 --> FS
FS --> Runc[runc starts container]
When kubelet asks containerd to pull nginx:1.27.1:
- Containerd talks to the registry, fetches the manifest for the platform matching the node’s architecture.
- It compares the manifest digest with what is already on disk.
- For each layer digest in the manifest, it fetches only the layers it does not have. Layers are content-addressed, so they are deduped across images.
- It unpacks the layers into a snapshotter’s directory (typically
under
/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/). - It starts the runtime (runc) with the unpacked root and the image’s config as the OCI runtime spec.
Tags are mutable, digests are not
This is the single most important supply-chain concept in production:
nginx:1.27.1is a tag. The registry owner can re-point this tag at a different image at any time. (Container registries usually forbid overwriting a tag’s digest silently, but it has happened and continues to happen.)nginx@sha256:abc123...is a digest reference. It points at one specific manifest and cannot be re-pointed without changing the digest.
Production Kubernetes should anchor on digests:
spec:
containers:
- name: web
image: nginx@sha256:abc123def456...
# NOT image: nginx:1.27.1 — that tag could change under you
kubectl set image and kustomize will rewrite digests back to
tags if the manifest is mutated carelessly. Production change
management requires digest pinning in version control and
admission control that rejects image: <name>:<mutable-tag>
patterns.
The runtime contract: CRI
Kubelet does not talk to runc directly. It talks to the
Container Runtime Interface (CRI), a gRPC API. The default
implementation is containerd; alternatives include CRI-O.
sequenceDiagram
autonumber
participant K as kubelet
participant C as containerd (CRI)
participant Reg as Registry
participant S as Snapshotter
participant R as runc
K->>C: PullImage(image=nginx:1.27.1, sandbox=...)
C->>Reg: GET /v2/nginx/manifests/1.27.1
Reg-->>C: manifest + config + layer digests
C->>Reg: GET /v2/nginx/blobs/<layer-digest>
Reg-->>C: layer tarball
C->>S: Unpack layers into snapshot
S-->>C: snapshot path
K->>C: CreateContainer(sandbox, container, mounts)
C->>R: runc create (OCI runtime spec)
K->>C: StartContainer(containerID)
C->>R: runc start
R-->>C: container PID
C-->>K: status updates
The CRI surface kubelet uses:
PullImage— fetches and unpacks an imageCreateSandbox— creates thepausenamespace holderCreateContainer/StartContainer/StopContainer/RemoveContainer— container lifecycleContainerStatus— runtime-side statusUpdateRuntimeConfig— applies Pod-level resources to the runtime
In production, the CRI error surface is where many kubelet
incidents originate. The Pod event Failed with reason
ErrImagePull or ImagePullBackOff is a CRI-layer error.
How kubelet authenticates to a registry
If the registry is anonymous (docker.io/library/*), kubelet
needs no credentials. For private registries, kubelet uses
credentials from imagePullSecrets on the ServiceAccount, the
node’s ~/.docker/config.json, or (in 1.34+) the kubelet’s
credential provider plugins.
kubectl get secret regcred -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d
{
"auths": {
"registry.example.com": {
"username": "ci-bot",
"password": "REDACTED",
"auth": "BASE64(user:pass)"
}
}
}
Image pull policy: when kubelet actually pulls
The Pod’s imagePullPolicy controls whether kubelet trusts the
cache:
| Policy | Behaviour |
|---|---|
Always | Pull on every Pod start, regardless of cache state |
IfNotPresent | Pull only if the digest is not in the local cache |
Never | Never pull; fail if the image is not cached |
Production defaults:
Alwaysfor any image referenced by mutable tag — without it, kubelet uses the cached layer set even if the tag has changedIfNotPresentfor any image referenced by digest — the cache check is by digest, so a cached image is authoritativeNeveronly in air-gapped clusters where the registry is deliberately unreachable and the cache is provisioned out of band
Kubelet’s defaults:
imagePullPolicy: Alwaysif the tag islatestor absentimagePullPolicy: IfNotPresentotherwise
Where image operations fail
Every ImagePullBackOff in production comes from one of:
- Registry unreachable. DNS, routing, firewall, TLS — all
the usual suspects. Check from the node:
This uses the same runtime kubelet uses; if it fails here, it fails for kubelet.crictl pull nginx:1.27.1 - Authentication failure. Wrong secret, expired token,
registry rejecting the ServiceAccount token. Look for
unauthorizedin kubelet logs:journalctl -u kubelet --since "5 min ago" | grep -i pull - Layer corrupt or partial. Disk full mid-pull, network
blip during layer fetch. The snapshotter will retry, but if
the cache is left in a partial state,
crictl rmpi(remove pod images) clears it. - Architecture mismatch. Image built for
amd64, node isarm64(Graviton, Apple Silicon in dev clusters). The manifest does not advertise a matching platform entry; pull fails with no manifest found. - Registry rate limit. Docker Hub throttles anonymous pulls. Production clusters should mirror or use a paid Docker Hub account.
Image lifecycle in the runtime cache
Containerd keeps pulled images in its content store. Kubelet
garbage-collects images it has not used in --image-gc-high-threshold
percent of disk (default 85%) and down to
--image-gc-low-threshold (default 80%). When the runtime’s
disk crosses the high threshold, kubelet deletes the oldest
unused images until it reaches the low threshold.
This means a node can lose its image cache if the disk pressure is high. Production nodes should:
- Have enough ephemeral storage for the full image set plus headroom
- Be monitored on
container_image_size_bytesandcontainer_fs_inodes_free - Have an alert when
kubelet_image_garbage_collectedis non-zero (kubelet emits an event when it GCs an image)
How to inspect what kubelet has cached
From any node:
crictl images
Output shows image ID (digest), tag, size, and the repository
the image was pulled from. This is the source of truth for “is
this image present on this node?” — not docker images, which
queries a different store if dockerd is also installed.
To list layers per image:
# Image ID from the `crictl images` output above:
IMAGE_ID=sha256:c2c45d506085d300b72a6d4b10e3dce104228080a2cf095fc38333afe0f70a1e
crictl inspecti "$IMAGE_ID"
Output includes info.imageSpec.config, info.imageSpec.rootfs.diffIds,
and info.imageSpec.os/info.imageSpec.architecture.
Multi-platform images and node selection
A multi-platform image (often a “fat manifest” or OCI image
index) lists platform-specific manifests. Kubelet, via
containerd, picks the manifest that matches the node’s
kubernetes.io/arch and kubernetes.io/os labels.
For production:
- Multi-arch images built for both
linux/amd64andlinux/arm64are the norm for clusters with mixed architecture. - A Pod that explicitly sets
nodeSelector: kubernetes.io/arch: arm64is the standard way to land a workload on Graviton nodes. - Mismatches fail at pull with no obvious error; the Pod event says “no matching manifest for linux/arm64” or similar.
Cross-course references
- The Docker course parts
XXVIII-Docker-ImagesandXXIX-Docker-Buildcover image layers, manifests, and multi-stage builds — the same model Kubernetes inherits. - The Docker course part
XXXVII-Docker-Registriescovers private registries, authentication, retention, and backup — the same surface kubelet integrates with. - The Linux course part
LXXVIII-Linux-Containerscovers the kernel primitives the runtime composes into the container/Pod abstraction. - The Observability course part
IX-Observability-Exporterscovers cAdvisor and node_exporter metrics that surface image pull times and runtime cache state into the monitoring stack.
Quiz
Knowledge check · 4 questions
Q1. Which OCI component is content-addressed and immutable, and is the right thing to pin in a production Kubernetes manifest?
Q2. An image tag is a reliable identity for a production Kubernetes workload because registries reject attempts to overwrite a tag.
Q3. A new Pod is stuck in `ImagePullBackOff` on every node it tries to land on. Diagnose from the kubelet logs and the runtime cache. What is the most likely cause, and how do you confirm it from the node?
Pod events: ``` 12:01:01 Normal Scheduled pod/web-7c8 -> node worker-04 12:01:02 Normal Pulling pod/web-7c8 pulling image "registry.example.com/team-a/web:v3.2.1" 12:01:32 Warning Failed pod/web-7c8 Failed to pull image ... rpc error: code = Unknown desc = Error response from daemon: failed to resolve reference "registry.example.com/team-a/web:v3.2.1": pull access denied, repository does not exist or may require authorization 12:02:02 Warning BackOff pod/web-7c8 Back-off pulling image ... ``` From the same node: ``` $ crictl pull registry.example.com/team-a/web:v3.2.1 FATA[0001] pulling image: failed to resolve reference ... pull access denied $ kubectl get imagepullsecrets -n team-a-prod NAME TYPE DATA AGE regcred kubernetes.io/dockerconfigjson 1 90d ```
Q4. Explain the difference between an image tag and an image digest. Why is pinning digests in production manifests considered a supply-chain best practice?
Passing score: 75%. Answers are checked in this browser.
Production discipline
- Pin digests in production manifests; reserve tags for humans and CI pipelines that rewrite digests back to tags.
- Set
imagePullPolicy: Alwaysfor any image referenced by a mutable tag;IfNotPresentis acceptable for digest-pinned images. - Provision node ephemeral storage with headroom for the full image set; image GC at 85% is too late if your cache is the deployment fast-path.
- Monitor
ImagePullBackOffevents and alert on sustained failures; most are auth or scope, not network. - Run a private registry (or a paid Docker Hub plan) with audit logs for tag mutations; image supply-chain incidents are detected at the registry, not in the cluster.