CRI — The Container Runtime Interface — Explained From Zero

CRI — The Container Runtime Interface — Explained From Zero

How Kubernetes learned to stop caring which container engine actually runs your containers — and what that meant for Docker's role in the Kubernetes world.

Picture a universal phone charging port that any phone manufacturer can build a plug for, instead of every phone brand needing its own custom charger built into every wall socket in every airport, hotel, and coffee shop on earth. Before that kind of standard exists, whoever builds the wall sockets has to specifically know about and support every single phone brand by name. The Container Runtime Interface, or CRI, is exactly this kind of standard, but for Kubernetes and container engines. Instead of Kubernetes needing to know the specific internal details of Docker, or any other particular container engine, CRI defines one shared, universal socket that any compliant container engine can plug into — and this single design decision is the reason Docker’s own direct, built-in role inside Kubernetes eventually changed.

1What Is CRI, and Why Did Kubernetes Need It?

The story starts with a very tangled early relationship between Kubernetes and Docker.

Kubernetes’ original, Docker-specific design

In its earliest years, Kubernetes was built with direct, specific knowledge of how Docker worked baked straight into its core node agent, called the kubelet. This worked fine while Docker was effectively the only widely used container engine, but it created a real architectural problem: every time a new container engine wanted to work with Kubernetes, someone had to modify Kubernetes’ own core codebase to understand that engine’s specific quirks, directly increasing the complexity and fragility of Kubernetes itself.

The idea behind a standard interface

The fix was to define a standard interface — a fixed, agreed-upon set of operations any container engine must support, described independently of any single engine’s internal implementation — and have Kubernetes talk only to that interface, never to a specific engine’s internals directly. This is exactly what CRI, the Container Runtime Interface, provides: a formally specified API that any container runtime can implement, letting the kubelet manage containers without ever needing engine-specific code inside Kubernetes’ own codebase again.

Simple Analogy

Before CRI, Kubernetes’ relationship with Docker was like a house wired at the factory specifically for one brand of lightbulb, with the wiring itself needing to be redone for any other brand. CRI is the equivalent of switching to a standard light socket — any bulb manufacturer whose bulb fits the standard socket just works, with zero changes needed to the house’s wiring.

What “runtime” means in this context

The word “runtime” here refers to the software actually responsible for creating, starting, stopping, and removing containers on a machine — pulling images, setting up isolation, and managing the container’s running process. Docker itself, containerd, and CRI-O are all examples of software that can fill this role, each with a different internal design, but any of them can serve Kubernetes equally well as long as they speak CRI.

Kubelet

The node agent

The Kubernetes component running on every node, responsible for making sure the containers Kubernetes wants running are actually running.

CRI

The standard interface

A defined API contract describing exactly how the kubelet talks to a container runtime, independent of that runtime’s internals.

Container Runtime

The actual engine

The software that implements CRI and does the real work of managing containers on the node.

Pod

Kubernetes’ basic unit

A group of one or more containers Kubernetes schedules and manages together — the actual object CRI operations ultimately serve.

i
Good to Know

CRI is a Kubernetes-specific standard, distinct from Docker itself and distinct from the OCI standards discussed later in this tutorial. It exists purely to define how Kubernetes talks to whatever runtime is managing containers on a given node.

2The Building Blocks: Kubelet, the CRI Shim, and the Runtime

A few distinct layers cooperate to turn a Kubernetes scheduling decision into a real running container.

1. The Kubelet

Runs on every node, continuously comparing what containers Kubernetes wants running against what’s actually running, and issuing CRI requests to reconcile any difference — without needing to know anything about the specific runtime underneath.

2. The CRI Shim

A small adapter component that translates generic CRI requests from the kubelet into whatever specific calls the underlying runtime actually understands. For runtimes built with CRI support natively in mind, this shim is effectively built in as a plugin rather than a separate piece of software.

3. The Container Runtime Itself

The software that actually does the low-level work — pulling images, creating containers, managing their lifecycle — fulfilling the requests the shim forwards to it.

4. The Lower-Level OCI Runtime

Beneath the container runtime, an even lower-level component (commonly a tool that implements the OCI Runtime Specification) handles the actual creation of Linux namespaces, control groups, and the final process launch — a layer discussed in more detail later in this tutorial.

flowchart TB
    A["Kubernetes Control Plane
(schedules a Pod)"] --> B["Kubelet
(on the target node)"] B -->|"CRI gRPC calls"| C["CRI Shim / Plugin"] C --> D["Container Runtime
(containerd, CRI-O, etc.)"] D --> E["OCI Runtime
(low-level container creation)"] E --> F["Running Container Process"]
FIG 1 — The layered path from a Kubernetes scheduling decision to a running container
!
Common Misconception

CRI is not a piece of software you install by itself. It’s a specification — a defined API contract. What actually runs on a node is a real container runtime that implements that specification, not “CRI” as its own standalone program.

3How CRI Actually Works: The API Underneath

Underneath the concept, CRI is a defined set of remote procedure calls, grouped into two clear services.

CRI is formally defined using a technology called gRPC, a framework for defining and calling remote procedures using structured, strongly typed messages. The specification splits its operations into two logically separate services, each focused on a distinct responsibility.

RuntimeService

Container and pod lifecycle

Operations for creating, starting, stopping, and removing pod sandboxes and the containers running inside them, along with checking their current status.

ImageService

Image management

Operations for pulling, listing, inspecting, and removing container images — kept as a separate concern from container lifecycle itself.

Why the split matters

Separating image management from container lifecycle reflects a real architectural insight: a runtime’s image-handling logic and its container-execution logic are genuinely different concerns, sometimes even implemented by different underlying components. Keeping the interface split this way gives implementers more flexibility in how they build their own runtime internally, as long as both services are exposed correctly to the kubelet.

The pod sandbox concept

CRI introduces the idea of a pod sandbox — a shared execution environment, including shared networking, that all containers within a single Kubernetes pod run inside together. Creating a pod sandbox happens as its own distinct CRI operation, separate from creating the individual containers that will run within it, reflecting how Kubernetes pods work as a genuinely shared unit rather than just a loose grouping label.

“CRI doesn’t ask a runtime to understand Kubernetes — it asks Kubernetes to describe exactly what it needs in a language every compliant runtime already speaks.”

4The Lifecycle of a Pod Through CRI

From a scheduling decision to a fully running set of containers, in a fixed, predictable order.

1

Pod Assigned to a Node

Kubernetes’ scheduler decides which node a new pod should run on, and that node’s kubelet becomes responsible for actually making it happen.

2

Pod Sandbox Created

The kubelet issues a CRI request to create the shared pod sandbox — establishing the shared network namespace the pod’s containers will all use.

3

Images Pulled

Using CRI’s image service, the runtime pulls any container images needed for this pod that aren’t already present on the node.

4

Containers Created and Started

For each container defined in the pod, the kubelet issues CRI requests to create it within the existing sandbox and then start it running.

5

Ongoing Status Reporting

The kubelet continues polling container and sandbox status through CRI, reconciling the pod’s actual state against what Kubernetes expects for as long as the pod exists.

6

Teardown

When the pod is deleted, the kubelet issues CRI requests to stop and remove its containers, and finally to remove the pod sandbox itself.

sequenceDiagram
    participant Kubelet
    participant Runtime as Container Runtime (via CRI)

    Kubelet->>Runtime: RunPodSandbox
    Runtime-->>Kubelet: Sandbox ready
    Kubelet->>Runtime: PullImage
    Runtime-->>Kubelet: Image ready
    Kubelet->>Runtime: CreateContainer
    Runtime-->>Kubelet: Container created
    Kubelet->>Runtime: StartContainer
    Runtime-->>Kubelet: Container running
    Kubelet->>Runtime: PodSandboxStatus (ongoing polling)
        
FIG 2 — The core CRI call sequence behind starting a pod

5Where Docker Fits In: Dockershim and Its Deprecation

This is the chapter most people are actually looking for when they ask about CRI and Docker together.

Docker predates CRI, and never spoke it natively

Docker’s own engine was never built to implement the CRI specification directly — it existed before CRI did, and CRI was designed with Docker’s existing behavior as one of its early reference points, not the other way around. To let Kubernetes keep using Docker after CRI was introduced, the Kubernetes project maintained a built-in translation layer, commonly called dockershim, that sat between the kubelet and Docker, translating CRI requests into calls Docker itself understood.

Why dockershim was eventually removed

Maintaining dockershim meant the Kubernetes project itself was responsible for keeping a Docker-specific compatibility layer working correctly, essentially recreating the exact kind of tightly coupled, engine-specific maintenance burden that CRI was originally introduced to eliminate. As other runtimes with native CRI support matured, the Kubernetes project made the decision to remove dockershim from the kubelet, simplifying Kubernetes’ own codebase and fully committing to the standard-interface model CRI was designed to enable.

AspectBefore Dockershim RemovalAfter Dockershim Removal
Docker support in kubeletBuilt-in, via the dockershim translation layerNo longer built into the kubelet directly
Maintenance burdenCarried by the Kubernetes project itselfRemoved from Kubernetes’ own codebase
Typical node runtimeDocker, translated through dockershimA native CRI implementation such as containerd or CRI-O
Building Docker imagesUnaffected either wayUnaffected either way
!
A Frequently Missed Point

This change affected how Kubernetes nodes run containers — it did not affect Docker as a development tool. Images built with Docker, using an ordinary Dockerfile, remain fully standard OCI-compliant images and run perfectly well on Kubernetes clusters using containerd or any other CRI-compliant runtime.

Simple Analogy

It’s similar to a device that never quite supported the new universal charging standard needing a special adapter to keep working with newer chargers. Eventually, once better-supporting devices are common, maintaining that one specific adapter forever stops making sense — even though the device itself, and the cable format it originally produced, both remain perfectly fine.

6CRI Implementations: containerd, CRI-O, and Others

Once CRI existed as a standard, several runtimes were built or adapted specifically to speak it natively.

containerd

A container runtime that, notably, originated as a core internal component of Docker’s own engine before being spun out as its own independent, widely adopted project. It includes a built-in CRI plugin, letting it serve Kubernetes directly without needing any separate shim layer.

CRI-O

A container runtime built specifically and exclusively to implement CRI for Kubernetes, with no broader general-purpose ambitions beyond that role — deliberately minimal and focused on being an excellent, lightweight Kubernetes-native runtime.

Other and specialized runtimes

Several other runtimes implement CRI as well, including options built around stronger isolation guarantees using lightweight virtual machines instead of standard Linux container isolation, letting clusters choose different runtime characteristics for different workloads through Kubernetes’ runtime class mechanism.

containerd
DOCKER’S FORMER CORE ENGINE
CRI-O
BUILT PURPOSELY FOR CRI
Others
SPECIALIZED ISOLATION MODELS

An important, often surprising detail: containerd being Docker’s former internal engine means that most modern Kubernetes clusters, running containerd directly as their node runtime, are actually running the very same underlying technology Docker itself is built on top of — just accessed through the standard CRI interface, without Docker’s own additional layers sitting on top of it.

7Advantages, Disadvantages, and Trade-offs

Standardizing the interface between Kubernetes and container engines paid off broadly, with a few real costs along the way.

Advantages

  • Removed engine-specific code from Kubernetes’ own core codebase, simplifying its long-term maintenance
  • Let multiple container runtimes compete and specialize, rather than locking Kubernetes to one engine’s roadmap
  • Enabled genuinely different runtime models — including stronger, VM-based isolation — through the same standard interface
  • Made it far easier for new runtimes to support Kubernetes, since the target interface is clearly and independently defined
  • Reduced Kubernetes’ exposure to any single container engine’s internal bugs or architectural decisions

Disadvantages / Trade-offs

  • The dockershim removal required real migration effort for clusters still relying on Docker directly as their node runtime
  • Teams and tooling built around Docker-specific node behavior needed to adapt to a different underlying runtime
  • The layered architecture (kubelet, shim or plugin, runtime, OCI runtime) adds conceptual complexity for newcomers to understand
  • Debugging a container issue sometimes means understanding which layer in this chain is actually responsible

8Security Considerations

The choice of CRI implementation is itself a meaningful security decision, not just a plumbing detail.

Isolation strength varies by implementation

Because CRI defines an interface rather than a specific isolation technology, different compliant runtimes can offer meaningfully different levels of isolation between containers and the host — from standard Linux namespace and control-group isolation to stronger, VM-based sandboxing for workloads that need to run genuinely untrusted or multi-tenant code.

The runtime as a high-privilege component

Whichever runtime implements CRI on a node runs with substantial privilege, since it’s directly responsible for creating and managing every container on that machine. Its own security posture — how quickly it receives and applies patches, how carefully its configuration is managed — directly affects the security of every workload scheduled onto that node.

Runtime Class

Match isolation to sensitivity

Use Kubernetes’ runtime class mechanism to run especially sensitive or untrusted workloads under a more strongly isolating CRI implementation where available.

Keep Runtimes Patched

Runtime-level vulnerabilities matter

Treat the node-level container runtime as critical infrastructure requiring the same prompt patching discipline as the kernel itself.

Socket Access Control

Protect the CRI endpoint

The local socket the kubelet uses to reach the runtime is itself sensitive — direct, uncontrolled access to it effectively grants control over every container on the node.

Consistent Configuration

Uniform hardening across nodes

Ensure security-relevant runtime configuration is applied consistently across every node in a cluster, not just on a handful configured by hand.

!
Common Mistake

Assuming all CRI-compliant runtimes provide identical security guarantees simply because they implement the same interface. CRI standardizes the API surface, not the underlying isolation technology or security posture of the implementation behind it.

9Monitoring, Logging, and Metrics

Diagnosing a container problem in Kubernetes often means knowing which layer to actually look at.

Kubelet-level visibility

The kubelet exposes events and status information reflecting its view of pod and container state as reported back through CRI, which is usually the first and most convenient place to check when a pod isn’t behaving as expected.

Runtime-specific logs

Each CRI implementation maintains its own logs, independent of Kubernetes’ own logging, covering everything happening below the CRI interface — image pulls, low-level container creation, and any errors that never bubble all the way up to the kubelet’s own reporting.

Debugging tools built for CRI

Dedicated command-line tools exist specifically for interacting with a CRI-compliant runtime directly, independent of Kubernetes itself, letting operators inspect pod sandboxes, containers, and images at the runtime level when kubelet-level information alone isn’t enough to diagnose a problem.

Metrics from the runtime layer

Modern CRI implementations typically expose their own metrics — image pull latency, container creation time, resource usage — giving visibility into runtime performance that sits below what Kubernetes’ own higher-level metrics capture.

“When a pod misbehaves, the honest first question isn’t ‘what’s wrong with my container’ — it’s ‘which layer, kubelet or runtime, actually knows what happened.'”

10Design Patterns and Anti-Patterns

Most of the friction teams hit with CRI comes from treating it as an implementation detail instead of a real architectural layer.

Pattern: choosing runtime class deliberately by workload

A mature pattern explicitly assigns different runtime classes to different categories of workload — standard isolation for typical internal services, stronger VM-based isolation for anything running less-trusted or externally supplied code — rather than treating every workload as needing identical isolation guarantees.

Pattern: treating the runtime as a managed, versioned dependency

Tracking the node-level CRI runtime’s version deliberately, upgrading it on a planned schedule alongside the rest of cluster infrastructure, avoids the surprise of a workload behaving differently simply because different nodes in the same cluster are silently running different runtime versions.

ANTI-PATTERN-01 Avoid
Problem

Assuming application or deployment behavior that happened to depend on Docker-specific quirks will work identically once the cluster’s node runtime changes to a different CRI implementation.

Why It’s Harmful

CRI standardizes the interface Kubernetes uses, not every last behavioral detail of the runtime underneath — subtle differences can surface in edge cases that weren’t tested against the new runtime.

Correct Approach

Explicitly test workloads and tooling against the actual runtime a cluster uses, rather than assuming Docker-era behavior carries over unchanged.

ANTI-PATTERN-02 Avoid
Problem

Debugging exclusively at the Kubernetes and kubelet level when a container problem actually originates below the CRI boundary, inside the runtime itself.

Why It’s Harmful

Time gets spent searching for a cause in the wrong layer entirely, while runtime-level logs that would have quickly explained the issue go unchecked.

Correct Approach

When kubelet-level information doesn’t fully explain a problem, check the runtime’s own logs and dedicated debugging tools directly, rather than staying confined to Kubernetes-level views alone.

11Best Practices and Common Mistakes

Small habits that keep the CRI layer from becoming an invisible source of confusion.

Practice

Know which runtime your cluster actually uses

Confirm the specific CRI implementation and version running on cluster nodes, rather than assuming it’s still Docker out of old habit.

Practice

Keep node runtimes consistent across a cluster

Standardize on the same CRI implementation and version across all nodes to avoid subtle, hard-to-reproduce behavioral differences.

Practice

Separate build tooling from runtime concerns

Recognize that building images with Docker locally and running them on a cluster using a different CRI implementation are two entirely independent, unrelated concerns.

Practice

Use runtime class for workloads that need it

Explicitly opt sensitive workloads into stronger isolation where a cluster’s available runtimes support it, rather than treating isolation strength as fixed.

Mistake

Conflating “building with Docker” and “running on Docker”

Assuming a Kubernetes cluster must be running Docker just because images were built using Docker’s own tooling elsewhere in the pipeline.

Mistake

Ignoring runtime-level logs during troubleshooting

Only ever checking Kubernetes’ own views of a problem, missing detail available directly from the runtime layer underneath.

12Real-World and Industry Examples

CRI’s adoption reshaped what “running Kubernetes” actually means at the infrastructure level industry-wide.

Major managed Kubernetes platforms

Leading cloud-managed Kubernetes offerings transitioned their default node runtimes to native CRI implementations like containerd well ahead of dockershim’s removal, reflecting how central this shift became to the broader Kubernetes ecosystem’s direction.

Standard Kubernetes distributions

Widely used Kubernetes distributions and installation tools now default to a native CRI runtime out of the box, with running Docker directly as the node-level runtime having become the unusual, legacy configuration rather than the default expectation.

Enterprise migration efforts

Organizations running large, long-lived Kubernetes clusters undertook deliberate, planned migration projects specifically to move node runtimes off Docker and dockershim ahead of its removal, treating it as significant enough infrastructure work to warrant its own dedicated planning.

Specialized isolation adoption

Organizations running genuinely multi-tenant or security-sensitive workloads on Kubernetes have increasingly adopted VM-based CRI implementations for those specific workloads, directly benefiting from CRI’s design allowing different runtimes to coexist and be selected per workload.

13Frequently Asked Questions

Q1Can I still build container images with Docker if my cluster doesn’t use Docker as its runtime?

Yes. Building images with Docker and running a Kubernetes cluster on a different CRI implementation are entirely separate concerns. Docker produces standard, OCI-compliant images that run perfectly well on any CRI-compliant runtime.

Q2Is CRI the same thing as the OCI Runtime Specification?

No. CRI defines how Kubernetes’ kubelet talks to a container runtime. The OCI Runtime Specification, discussed as its own lower layer in this tutorial’s architecture chapter, defines how that runtime actually creates a container’s low-level isolation on the machine. They’re related, complementary standards operating at different layers.

Q3Does removing dockershim mean Docker is no longer relevant to Kubernetes at all?

It means Docker is no longer used directly as the node-level container runtime by default. Docker remains an extremely common tool for building images in development and CI pipelines that ultimately get deployed to Kubernetes clusters running a different, native CRI runtime.

Q4Why does containerd, which comes from Docker, not have the same dockershim problem Docker itself had?

Because containerd includes a CRI plugin built directly into it, letting it speak CRI natively without any separate translation layer — unlike Docker’s full engine, which was never built with native CRI support and required the external dockershim adapter instead.

Q5Do all CRI-compliant runtimes provide the same level of container isolation?

No. CRI standardizes the interface, not the isolation technology underneath. Different implementations can offer meaningfully different isolation strength, from standard Linux container isolation to stronger, VM-based sandboxing.

Q6How do I find out which container runtime my Kubernetes nodes are actually using?

Kubernetes exposes each node’s runtime name and version as part of its standard node status information, making it straightforward to confirm exactly which CRI implementation a given cluster’s nodes are running.

14Summary and Key Takeaways

The Container Runtime Interface exists to answer a question Kubernetes’ early, Docker-specific design couldn’t answer well: how does an orchestrator manage containers without being permanently tied to one specific engine’s internals? By defining a fixed, independent API contract — split cleanly into runtime and image services, and built around the shared pod sandbox concept — CRI let Kubernetes stop caring which specific engine actually runs containers on a node, as long as that engine speaks the standard interface. This is the direct reason Docker’s own role changed: never having been built to speak CRI natively, Docker required a translation layer that the Kubernetes project eventually removed once better-suited, natively compliant runtimes like containerd and CRI-O matured — a change that affected how clusters run containers, not how Docker builds them.

Key Takeaways

  • CRI is a specification, not a piece of software — an API contract any compliant container runtime can implement.
  • It exists to decouple Kubernetes from any single container engine’s internals, replacing engine-specific code with one standard interface.
  • Docker was never built to speak CRI natively, which required the dockershim translation layer that Kubernetes eventually removed.
  • Building images with Docker and running a cluster’s runtime are separate concerns. Docker-built images remain fully compatible with any CRI-compliant runtime.
  • containerd, itself originally part of Docker’s engine, is one of the most widely used native CRI implementations today.
  • Different CRI implementations can offer different isolation strength, letting clusters match runtime choice to workload sensitivity via runtime classes.
  • Troubleshooting sometimes requires looking below Kubernetes’ own view, directly at runtime-level logs and tools, to find the real cause of a problem.