Containerization: Images, Container Runtimes, CRI and the OCI Explained
A container is an isolated process running on a shared host kernel, boxed in by Linux namespaces for isolation and cgroups for resource control, and started from a portable, layered image. Everything Kubernetes does rests on this foundation: every Pod you deploy is one or more containers, pulled as images from a registry and executed by a container runtime that the kubelet drives through the Container Runtime Interface. The KCNA exam tests this layer directly, asking what containers actually are, how they differ from virtual machines, how images and registries work, which runtimes Kubernetes supports and how it chooses between them, how Pods pull images, and what the Open Container Initiative standardizes. It also expects you to know the story of the Dockershim removal and why Docker-built images kept working afterward. This lesson gives you that whole picture at the recognition depth the exam demands.
On this page8 sections
- Define a container as an isolated process built on Linux namespaces and cgroups, contrast it with a virtual machine, and place seccomp and AppArmor alongside them
- Describe container images as stacks of read-only layers identified by tags and digests, and explain how short image names expand and which kind of registry suits which need
- Recognize the basic Dockerfile instructions, order them for layer caching, and explain why deleting files in a later layer does not shrink an image
- Explain the Container Runtime Interface, identify containerd and CRI-O and their default OCI runtimes, and inspect a node with crictl
- Choose a low-level runtime per Pod with a RuntimeClass, and tell runc, crun, gVisor and Kata Containers apart
- State what the Open Container Initiative standardizes, why the Dockershim removal did not break Docker-built images, and when cri-dockerd is needed
- Recognize what Podman, Buildah and Skopeo each do, including the Podman–Kubernetes YAML round trip
- Predict a Pod's default imagePullPolicy, authenticate pulls from private registries, and outline how a Pod's containers share a network namespace
What a container actually is
A container is not a small virtual machine. It is an ordinary Linux process, or group of processes, that the kernel has been told to isolate and constrain. Two kernel features do the work. Namespaces control what the process can see: the PID namespace gives it its own process tree, the network namespace its own interfaces and IP address, the mount namespace its own filesystem view, plus UTS (hostname), IPC and user namespaces. Control groups (cgroups) control what the process can use: how much CPU, memory and I/O it may consume. Add a layered image as its root filesystem and you have a container.
Namespace isolation is one-way. A container gets its own PID namespace by default, so inside it the application sees only its own processes, often starting at PID 1. The host, however, sits outside that namespace and sees every container process in its own process list, because those processes really are host processes. Sharing the host's process namespace is an explicit opt-in (--pid=host in Docker, hostPID: true on a Pod), never the default. The exception is a VM-sandboxed runtime such as Kata Containers: there the processes run inside a lightweight guest VM, and the host sees only the VM, not the processes inside it.
A base image supplies user-space files only: libraries, shells, package managers and configuration. It never supplies a kernel. A container built FROM ubuntu or FROM alpine still runs on whatever kernel the host is running; the distribution name describes the files inside the image, not the operating system kernel underneath it.
The decisive difference from a virtual machine is the kernel. Every container on a host shares that host's kernel; a VM runs on a hypervisor and carries its own complete guest operating system and kernel. That single fact explains all the trade-offs the exam asks about:
| Aspect | Container | Virtual machine |
|---|---|---|
| Kernel | Shares the host kernel | Own guest kernel per VM |
| Virtualizes | The operating system | The hardware |
| Startup | Milliseconds to seconds | Seconds to minutes (full OS boot) |
| Size | Megabytes (image layers) | Gigabytes (full OS disk) |
| Density | High: many per host | Lower: heavy per-VM overhead |
| Isolation | Kernel-level; weaker boundary | Hardware-level; stronger boundary |
Because the kernel is shared, containers are lighter and faster to start but offer a weaker isolation boundary than VMs, which is why sensitive multi-tenant platforms sometimes wrap containers in lightweight VMs or sandboxed runtimes (covered below). For the exam, remember the one-liner: containers virtualize the operating system, VMs virtualize the hardware.
Namespaces and cgroups are not the only controls. Two kernel security layers restrict what a container is allowed to do: seccomp filters which system calls the process may make, and AppArmor (or SELinux on some distributions) confines which files, capabilities and network operations a program may use. In Kubernetes both are set in a Pod or container securityContext. Keep the four apart:
| Mechanism | What it controls |
|---|---|
| Namespaces | What the process can see (processes, network, mounts, hostname) |
| cgroups | How much it can use (CPU, memory, I/O) |
| seccomp | Which system calls it may make |
| AppArmor / SELinux | Which files, capabilities and operations it may access |
All four harden a container, but none of them gives it its own kernel: every container on the host still shares the host kernel. Only a VM-based or kernel-intercepting runtime changes that.
Building images with a Dockerfile
Images are typically built from a Dockerfile, a plain-text recipe of instructions executed top to bottom. The exam expects recognition of the core instructions, not fluency in writing them. FROM names the base image the build starts from. RUN executes a command during the build, such as installing packages. COPY brings files from the build context into the image. EXPOSE documents the port the application listens on. CMD and ENTRYPOINT define what runs when a container starts from the image: ENTRYPOINT is the fixed executable, CMD supplies default arguments that are easy to override.
FROM node:20-alpine WORKDIR /app COPY package.json . RUN npm install COPY . . EXPOSE 3000 CMD ["node", "server.js"]
Each instruction that changes the filesystem produces a new layer, and the builder caches layers: if an instruction and everything before it are unchanged, the cached layer is reused instead of rebuilt. The cache is ordered, so the first changed instruction invalidates its own layer and every layer after it. That is why the example copies only the dependency manifest (package.json) and runs the install before copying the rest of the source tree: editing application code then invalidates only the final COPY . . layer, and the expensive install layer comes from cache. Copy the whole source tree first and every code edit forces a full reinstall.
Layers are also why deleting a file in a later instruction does not shrink the image. Earlier layers are immutable, so a later RUN rm only adds a whiteout marker that hides the file in the merged view; the bytes still ship inside the earlier layer. To keep a temporary file out of the image, create and remove it within the same RUN instruction (for example, download, install and clean the package cache in one command), or use a multi-stage build so the file never reaches the final stage.
You build and tag with docker build -t myapp:1.0 . and publish with docker push. Two size-reduction ideas are worth recognizing: small base images such as alpine or distroless images that omit shells and package managers, and multi-stage builds, where one stage compiles the application and a final minimal stage copies in only the finished binary, leaving compilers and build tools out of the shipped image. Smaller images pull faster and expose less attack surface.
Container runtimes and the CRI
The container runtime is the node-level software that actually pulls images and runs containers. Kubernetes does not run containers itself: on every node, the kubelet asks the runtime to do it, speaking a standard gRPC API called the Container Runtime Interface (CRI). The CRI defines operations like pull this image, create this sandbox, start this container, so any runtime that implements it can plug into Kubernetes without changes to the kubelet. This pluggability is the whole point: the interface decouples Kubernetes from any single vendor's runtime.
Two CRI runtimes dominate real clusters. containerd is a graduated CNCF project that began life inside Docker and was donated as a standalone runtime; it is the default in most managed Kubernetes services. CRI-O is a lighter runtime created specifically to serve the CRI for Kubernetes and nothing else; it is the default in Red Hat OpenShift. Functionally both do the same job for the kubelet.
| Aspect | containerd | CRI-O |
|---|---|---|
| Origin | Extracted from Docker, donated to CNCF | Built by the Kubernetes community, led by Red Hat |
| Scope | General-purpose runtime, also used outside Kubernetes | Purpose-built to implement the CRI for Kubernetes only |
| Common home | Default in most managed Kubernetes offerings | Default in OpenShift |
One more layer completes the picture: these are high-level runtimes that manage images and container lifecycles, and they delegate the final step of creating the isolated process to a low-level OCI runtime, which sets up the namespaces and cgroups and starts the process. containerd uses runc by default; CRI-O has defaulted to crun, a lightweight OCI runtime written in C, since CRI-O v1.31. Both are implementations of the same OCI runtime spec. The chain is always kubelet → CRI → high-level runtime → OCI runtime: the kubelet never calls runc or crun directly.
To inspect containers on a node, use crictl, the command-line client for the CRI. It shows the node the way the kubelet sees it: crictl pods lists Pod sandboxes, crictl ps -a lists containers including exited ones, and crictl logs reads a container's output, which works the same on containerd and CRI-O nodes. containerd's own client, ctr, talks to containerd directly and bypasses the CRI, so it has no notion of Pods and shows Kubernetes containers only as raw entries in containerd's k8s.io namespace; it is a containerd debugging tool, not a Kubernetes one.
The Dockershim removal, and why Docker images still work
Docker Engine predates the CRI and never implemented it. To keep supporting Docker as a runtime, the Kubernetes project maintained an adapter inside the kubelet called the Dockershim, which translated CRI calls into Docker Engine API calls. Maintaining a vendor-specific shim inside core Kubernetes was a growing burden, especially since Docker itself uses containerd underneath, so the shim added a hop that contributed nothing. Kubernetes deprecated the Dockershim in version 1.20 and removed it in version 1.24. Since 1.24, the kubelet only speaks CRI, and nodes use a CRI runtime such as containerd or CRI-O; teams that still want Docker Engine as the runtime can install cri-dockerd, an externally maintained adapter (maintained by Mirantis) that implements the CRI in front of Docker Engine. No feature gate or kubelet flag brings the in-tree Dockershim back; it was deleted from the code.
The distinction matters for any node tooling that calls the Docker Engine API directly, through the Docker socket (/var/run/docker.sock), for example scripts that run docker ps on the node or older logging, monitoring and security tools. Switching the node to containerd removes that socket: containerd serves its own API and the CRI, not the Docker Engine API, even though Docker Engine uses containerd internally. Such tooling has two options: update it to work with the CRI runtime (or the Kubernetes API), or keep Docker Engine and connect the kubelet to it through cri-dockerd.
The exam's favorite follow-up: did this break images built with Docker? No, and understanding why is the point. An image built by docker build is an OCI-compliant image, the same standardized format every runtime consumes. containerd and CRI-O pull and run Docker-built images exactly as before. What was removed was Docker Engine's role as the node runtime that Kubernetes drives; the image format was never Docker-specific in any way that mattered, because it is governed by an open standard.
So the correct summary, as opposed to the common misreading that Kubernetes dropped Docker support entirely, is: Kubernetes removed its adapter for Docker Engine as a runtime; developers still build images with Docker every day, push them to registries, and run them on Kubernetes clusters whose nodes use containerd or CRI-O.
The Open Container Initiative (OCI)
The interoperability that made the Dockershim removal painless comes from the Open Container Initiative, an open governance project established in 2015 under the Linux Foundation to standardize container formats and runtimes. Docker seeded it by donating its image format and its runtime code, and the industry, including the major cloud providers and container vendors, maintains the specifications together. The OCI publishes three specifications the exam expects you to name:
- The image spec defines what a container image is: the layout of its filesystem layers, its manifest and its configuration metadata. Any OCI image can be built by one tool and run by another.
- The runtime spec defines how to run a container from an unpacked image: the configuration of namespaces, cgroups, mounts and process settings, and the lifecycle a runtime must implement.
- The distribution spec defines the registry API for pushing and pulling images, so any client can talk to any registry.
The reference implementation of the runtime spec is runc, the low-level runtime Docker donated, and containerd's default. Other implementations of the same spec can replace it without changing Kubernetes:
| OCI runtime | How it isolates | Used for |
|---|---|---|
runc | Namespaces and cgroups on the shared host kernel | Reference implementation; containerd's default |
crun | Same model as runc, written in C for a smaller, faster binary | CRI-O's default since v1.31 |
gVisor (runsc) | A user-space kernel intercepts the container's system calls, so they do not reach the host kernel directly | Sandboxing untrusted workloads |
| Kata Containers | Each Pod runs inside a lightweight VM with its own guest kernel | VM-strength isolation with a container workflow |
A cluster can offer several of these at once. A RuntimeClass object names a configuration and points, through its handler field, at a runtime handler configured in the node's CRI runtime (for example a containerd or CRI-O entry for Kata). A Pod selects it with spec.runtimeClassName; Pods that set nothing use the node's default runtime. That is how one cluster runs ordinary workloads on runc and sandboxes only the untrusted ones.
Keep the two interface layers straight, because questions blur them deliberately: OCI standardizes the artifacts and the low-level runtime behavior, industry-wide; CRI is the Kubernetes-specific API the kubelet uses to talk to a high-level runtime. containerd sits in the middle, speaking CRI upward to the kubelet and driving an OCI runtime, runc, downward.
Running containers: docker and podman
You should recognize the everyday commands of the two mainstream client tools. With Docker, docker run creates and starts a container from an image, with familiar flags: -d detaches it to run in the background, -p 8080:80 publishes container port 80 on host port 8080, -e sets an environment variable, and --name labels it. Supporting commands include docker ps to list running containers, docker logs to read output, docker exec to run a command inside a running container, and docker stop and docker rm to end and remove it.
docker run -d --name web -p 8080:80 nginx:1.27 docker ps docker logs web docker exec web nginx -v docker stop web
Podman is the main alternative, and its differences are exactly what the exam probes. Docker uses a client-server design: the CLI talks to a long-running privileged daemon that owns every container. Podman is daemonless, running containers as ordinary child processes with no central daemon, and it emphasizes rootless operation, letting unprivileged users run containers, which shrinks the attack surface. Its command line is deliberately compatible with Docker's, so podman run, podman ps and podman build work as you would expect; both tools produce and consume the same OCI images.
Podman also has pods: podman pod create groups containers that share a network namespace, the same idea as a Kubernetes Pod. That enables a round trip with Kubernetes YAML. podman kube generate exports running containers or a pod as Kubernetes YAML, and podman kube play does the reverse, reading a Kubernetes Pod or Deployment YAML and running it locally under Podman with no cluster involved. Don't confuse these with podman generate systemd, which writes systemd unit files to run containers as local services (now deprecated in favour of Quadlet files); it targets the host's init system, not Kubernetes.
Two companion tools cover the rest of the image lifecycle. buildah builds OCI images, including from Dockerfiles, without a daemon. skopeo works on images that sit in registries: skopeo inspect reads a remote image's manifest and configuration without pulling it, and skopeo copy moves an image straight from one registry to another without storing it locally first and without any daemon, which makes it a natural fit for mirroring images in CI pipelines.
For KCNA, the takeaways are recognition-level: these are developer-facing tools for building and running individual containers on a machine. On a Kubernetes node, the kubelet and the CRI runtime do that job; you do not run docker commands to operate Pods.
How Pods use container images
Kubernetes never runs a bare container; its smallest deployable unit is the Pod, which wraps one or more containers. Each container in a Pod names its image in the spec, and when the Pod is scheduled to a node, the kubelet instructs the CRI runtime to pull each image from its registry and start the containers.
Private registries need credentials at pull time, before any container exists. You store them in a Secret of type kubernetes.io/dockerconfigjson (for example with kubectl create secret docker-registry) and reference it in the Pod's imagePullSecrets list, or attach it to the Pod's ServiceAccount so every Pod using that account inherits it. The kubelet passes those credentials to the runtime for the pull. Mounting the same Secret as a volume or exposing it through env or envFrom does not help: those deliver data to the running container, which only exists after the image has been pulled.
The imagePullPolicy field controls when the node pulls: Always checks the registry on every container start (if the image's digest matches the cached copy, nothing is downloaded), IfNotPresent pulls only when the image is missing from the node's local cache, and Never uses only the local cache and fails if the image is absent. If you leave the field empty, Kubernetes sets it when the object is created, based on the image reference:
| Image reference | Default imagePullPolicy |
|---|---|
myapp:latest or no tag (myapp) | Always |
Any other tag (myapp:1.4) | IfNotPresent |
A digest (myapp@sha256:…) | IfNotPresent |
That default has a known trap. If someone re-pushes a new build under an existing tag such as myapp:1.4, nodes that already cached the old build keep running it under IfNotPresent, while newly added nodes pull the new one, so the same Deployment runs two different builds. No pull policy fixes this: Always just fetches whatever the tag points at right now, and Never runs whatever happens to be cached. Only referencing the image by digest pins the content, because a digest cannot be re-pointed.
When a pull fails, because the tag does not exist, the registry needs credentials, or the network is down, the Pod's container reports ErrImagePull and then ImagePullBackOff as Kubernetes retries with increasing delays; recognizing that status as an image problem, not a scheduling or crash problem, is a standard exam discrimination.
Containers in the same Pod are more intimate than neighboring containers on a host: they share the same network namespace, meaning one IP address for the whole Pod and the ability to reach each other over localhost, and they can share storage volumes. The runtime holds those shared namespaces in a minimal infrastructure container, often called the pause container, that anchors the Pod so application containers can restart without the Pod losing its network identity. This is the mechanic behind the sidecar pattern, where a helper container, such as a log shipper or proxy, runs beside the main application container in one Pod.
Tip. KCNA probes containerization with definitional and short scenario questions: which kernel features containers are built on (namespaces for visibility, cgroups for resources, with seccomp and AppArmor as separate restriction layers), containers versus VMs (the shared host kernel is the trigger), and which body standardizes image and runtime formats (OCI, not CNCF or CRI). Expect the Dockershim question in some form, including when cri-dockerd is needed. Know which layer each name lives at (CRI versus OCI, containerd versus runc, crictl versus ctr), and know that RuntimeClass is how a Pod picks a sandboxed runtime. Image questions turn on mutable tags versus digests, short-name expansion, Dockerfile layer caching and whiteouts, the default imagePullPolicy for each kind of reference, and why a pull secret must be referenced in imagePullSecrets rather than mounted. Tool questions ask what Podman, Buildah and Skopeo each do.
- A container is an isolated process on a shared host kernel: namespaces control what it sees, cgroups control what it uses, seccomp and AppArmor control what it may do.
- Containers virtualize the operating system; VMs virtualize the hardware. A base image supplies user-space files only, never a kernel.
- Isolation is one-way: containers cannot see host processes, but the host sees every container process unless a VM-sandboxed runtime such as Kata is used.
- Images are stacks of immutable read-only layers; tags are mutable pointers, digests are immutable content hashes. nginx expands to docker.io/library/nginx:latest.
- Order Dockerfiles so dependency installs come before copying source; clean up temporary files in the same RUN or use a multi-stage build, because a later delete does not shrink the image.
- The OCI publishes the image, runtime and distribution specs (runc is the reference runtime). The kubelet drives the runtime through the CRI; containerd defaults to runc, CRI-O to crun, and crictl is the CRI-level debugging tool.
- A RuntimeClass maps a name to a runtime handler, letting Pods opt into sandboxed runtimes such as gVisor or Kata via runtimeClassName.
- Kubernetes removed the Dockershim in 1.24; Docker-built images still run because they are OCI images, and cri-dockerd is the only way to keep Docker Engine as the node runtime.
- Images tagged latest or untagged default to imagePullPolicy Always, other tags and digests to IfNotPresent; only a digest pins content, and private pulls need imagePullSecrets.
- Podman is daemonless and rootless-friendly, runs pods locally and round-trips Kubernetes YAML with podman kube generate and podman kube play; skopeo copies images between registries without a daemon.
Frequently asked questions
What is the difference between a container and a virtual machine?
A container is an isolated process that shares the host's kernel, using Linux namespaces for isolation and cgroups for resource limits, so it starts in milliseconds and weighs megabytes. A virtual machine runs on a hypervisor with its own complete guest operating system and kernel, giving a stronger hardware-level isolation boundary at the cost of gigabytes of footprint and slower boot. In short: containers virtualize the OS, VMs virtualize the hardware.
Did Kubernetes remove support for Docker?
Kubernetes removed the Dockershim, its built-in adapter that let the kubelet drive Docker Engine as a node runtime, deprecating it in 1.20 and removing it in 1.24. It did not break Docker-built images: docker build produces standard OCI images, which CRI runtimes like containerd and CRI-O pull and run exactly as before. You still build with Docker and deploy to Kubernetes; only Docker Engine's role as the node's runtime went away, and cri-dockerd exists for teams that want it back.
What is the Container Runtime Interface (CRI)?
The CRI is the gRPC API through which the kubelet on each node talks to the container runtime, covering operations like pulling images and creating, starting and stopping containers. It decouples Kubernetes from any specific runtime: anything implementing the CRI plugs in without kubelet changes. The two mainstream CRI runtimes are containerd, the default in most managed Kubernetes services, and CRI-O, a Kubernetes-only runtime that is the default in OpenShift.
What does the Open Container Initiative standardize?
The OCI, founded in 2015 under the Linux Foundation, maintains three specifications: the image spec, defining the layered container image format; the runtime spec, defining how a runtime creates and manages a container from an image; and the distribution spec, defining the registry API for pushing and pulling images. Its reference runtime is runc; crun, gVisor and Kata Containers are other implementations of the runtime spec. These standards are why an image built with Docker, Podman or Buildah runs identically under containerd or CRI-O.
What is the difference between a container image tag and a digest?
A tag is a mutable, human-readable label like nginx:1.27 that the repository owner can re-point to a different image at any time, which is why relying on latest in production is risky. A digest is the immutable SHA-256 content hash of an image, referenced as name@sha256:..., and always identifies exactly one build. Pinning by digest guarantees every node runs byte-for-byte the same image.
How is Podman different from Docker?
Docker uses a client-server design where the CLI talks to a long-running privileged daemon that owns all containers. Podman is daemonless, launching containers as ordinary child processes, and is designed for rootless operation so unprivileged users can run containers, reducing attack surface. Its CLI is intentionally Docker-compatible, and both tools build and run the same OCI-standard images, so skills and images transfer directly between them.
What is the default imagePullPolicy in Kubernetes?
If imagePullPolicy is left empty, Kubernetes sets it when the Pod (or its template) is created: Always when the image uses the latest tag or no tag at all, and IfNotPresent for any other tag or for an image referenced by digest. Because IfNotPresent reuses whatever a node has cached, re-pushing a new build under an existing tag can leave different nodes running different builds; only referencing the image by its digest guarantees every node runs the same content.
What is a RuntimeClass in Kubernetes?
A RuntimeClass is a cluster-scoped Kubernetes object that names a container runtime configuration and maps it, through its handler field, to a runtime handler configured in the node's CRI runtime such as containerd or CRI-O. A Pod opts in by setting spec.runtimeClassName; Pods that do not set it use the node's default runtime. Clusters use it to run untrusted workloads under a sandboxed OCI runtime such as gVisor or Kata Containers while everything else stays on runc or crun.
Source
This lesson covers the "Kubernetes Fundamentals" domain of the official KCNA exam guide. Vendors revise their guides — check the source for the current version.
- KCNA certification page and curriculum — The Linux Foundation
Sign up free to mark lessons complete, bookmark topics and track your exam readiness.