$ skip to content
devresurge

05 — OCI images: suitcases of files + config

ELI5 hook

An image is a suitcase: neatly stacked layers of clothes (filesystem diffs) plus a luggage tag (config: which binary to run, env vars, working directory). A container is opening that suitcase into a room (namespaces + cgroup) and starting to live there.

Mental model

TermMeaning
OCI ImageContent-addressed bundle: layers + config + manifest
LayerTar diff (usually) — add/change/delete files
DigestHash (sha256:…) — immutable identity
TagMutable nickname (:latest, :1.2.3) pointing at a digest
RootfsMerged view of layers mounted as the container’s /

Tags move. Digests don’t. Production pins digests (or immutable tags you control).

What Linux / OCI does

Manifest → config → layers

flowchart TB
  tag["tag: app:1.0"] --> digest[manifest digest]
  digest --> cfg[config JSON]
  digest --> L1[layer blob]
  digest --> L2[layer blob]
  L1 --> rootfs[merged rootfs]
  L2 --> rootfs
  cfg --> runtime[entrypoint env cwd]

Overlay mounts

Runtimes typically use overlayfs: lower layers read-only, thin upper layer writable (the container’s changes). Delete the container → upper layer gone (unless you volume-mount data elsewhere).

# Explore locally with Docker/podman (implementation detail varies)
docker pull alpine:3.20
docker image inspect alpine:3.20 --format '{{.Id}} {{.RootFS.Layers}}'

Building is just stacking diffs

Each Dockerfile RUN / COPY usually creates a layer. Fewer, purposeful layers → smaller pulls and clearer caching — but correctness > micro-optimizing layer count.

How Kubernetes uses it

  1. You set image: registry.example/app@sha256:… (or a tag)
  2. kubelet asks the CRI runtime (containerd) to pull if missing
  3. Runtime stores content in its content store, mounts rootfs, starts process
ConcernPractice
Supply chainSign images (cosign), verify in admission
DriftPin digest; avoid :latest in prod
Pull secretsimagePullSecrets for private registries
imagePullPolicyIfNotPresent / Always / Never

ImagePullBackOff means: registry auth, typo, network, or rate limit — not “Kubernetes is broken.”

Lab — see layers and config

# Pull and show history (layer commands)
docker pull alpine:3.20
docker history alpine:3.20

# Config bits that become container process settings
docker image inspect alpine:3.20 --format 'Entrypoint={{.Config.Entrypoint}} Cmd={{.Config.Cmd}} User={{.Config.User}}'

# Digest vs tag
docker image inspect alpine:3.20 --format '{{index .RepoDigests 0}}'

Optional — export and peek (large; skip if low disk):

docker create --name tmp-alp alpine:3.20
docker export tmp-alp | tar -t | head
docker rm tmp-alp

Gotchas

  • :latest lies — Yesterday’s latest ≠ today’s; nodes may have different cached images with IfNotPresent.
  • Fat images — Debug tools in prod images expand attack surface; prefer distroless/minimal + separate debug profiles.
  • Writable rootfs by default — Combine with readOnlyRootFilesystem and emptyDir for /tmp.
  • Multi-arch — Manifest lists pick amd64/arm64; wrong arch → weird exec format errors.

Checkpoint

Images package a filesystem + process recipe. Runtimes turn that into namespaces + cgroup + exec.

Next: 06 — containerd & CRI.


Quiz arc: after chapters 05–07 → /quizzes/k8s-containers-runtime/