Dockershim Deprecation in Docker

Dockershim Deprecation in Docker

A ground-up, zero-jargon guide to what dockershim actually was, why Kubernetes removed it, and what it really means for anyone still running Docker underneath a Kubernetes cluster.

Imagine two companies that speak entirely different languages trying to do business together for years, relying on a single, overworked human translator sitting in the room for every meeting. The translator does an admirable job, but they’re not actually an employee of either company — they’re a third party, patched in as an afterthought, and every new feature either company wants to discuss first has to be explained to, and re-explained by, that same translator before it can be understood on the other side. Eventually, one company decides it’s healthier for everyone to simply require that all business partners speak its language natively, rather than keep maintaining a translator whose job was always meant to be temporary. This is, in essence, the story of dockershim: a translator Kubernetes relied on for years to talk to Docker, and a translator Kubernetes eventually decided to retire. This guide explains exactly what that translator did, why it existed in the first place, and what changed the day it was removed.

1Introduction & Core Concepts

A few foundational definitions before diving into architecture and history.

Kubernetes is a system for automatically deploying, scaling, and managing containerized applications across a cluster of machines. Kubernetes itself does not actually create or run containers directly — it delegates that job to a separate piece of software running on each machine (called a node) known as a container runtime. Kubernetes was deliberately designed to not care which specific container runtime a node uses, as long as that runtime can perform a standard set of operations: create a container, start it, stop it, report its status, and so on.

Docker was the original, dominant container runtime in the early years of the container ecosystem, and Kubernetes’s very first releases were built with tight, direct integration to Docker specifically, before the idea of supporting multiple interchangeable runtimes even existed as a formal design goal. As Kubernetes matured and other container runtimes emerged, the project introduced a standardized interface — the Container Runtime Interface (CRI) — so that any runtime implementing this interface could work with Kubernetes without Kubernetes needing custom, hardcoded logic for each one individually.

Here’s the core problem this guide is about: Docker, as a piece of software, was never built to natively speak this new CRI interface — it predates CRI’s existence and has its own separate, independent API. Dockershim was the piece of adapter code Kubernetes maintained specifically to translate between the standardized CRI calls Kubernetes made internally, and the different, Docker-specific API calls that Docker Engine actually understood. Dockershim deprecation refers to Kubernetes’s decision, announced well in advance and finalized in the Kubernetes 1.24 release, to remove this translator entirely from its own codebase.

Everyday Analogy

Picture an international airport that adopts a single, standardized security-screening protocol every airline must follow to use its gates. Most airlines updated their own ground crews to follow this protocol directly. One older, well-established airline hadn’t updated its own crew training, so the airport temporarily assigned its own staff to shadow that airline’s crew at every gate, manually translating each step of the standard protocol into the airline’s older, different internal procedure. Dockershim was exactly that shadow staff — a temporary bridge the airport (Kubernetes) provided, not something the airline (Docker) built or maintained itself. When the airport eventually decided that permanently staffing this bridge wasn’t sustainable, it announced the change well ahead of time and required that airline to either adopt the standard protocol on its own, or use a different, standards-compliant ground crew instead.

This is the single most important, and most commonly misunderstood, fact about the entire topic: dockershim deprecation did not mean Docker itself was deprecated, broken, or removed. Docker continued to work exactly as it always had for running standalone containers, for local development, and for Docker Compose workflows. What changed was specifically, narrowly, Kubernetes’s own built-in bridge code that let a Kubernetes cluster use Docker Engine directly as its container runtime.

It’s worth being explicit, too, about the difference between “deprecation” and “removal” as Kubernetes uses those terms, since the two are often conflated in casual conversation but describe genuinely different moments in a formal process. Deprecation is an announcement: the project signals that a feature is expected to be removed at a specific point in the future, giving users advance warning and time to plan, while the feature itself continues to function normally in the meantime. Removal is the later, separate event where the feature actually stops being available. Dockershim’s deprecation was announced roughly a year before its actual removal in Kubernetes 1.24 — meaning for that entire year, dockershim kept working exactly as before, while the community had a clear, publicly documented deadline to plan around rather than being caught by surprise.

Fact

Kubernetes-Specific

Dockershim was Kubernetes code, living inside the kubelet, not a component of Docker itself.

Fact

A Translation Layer

It converted standardized CRI calls into Docker Engine’s own, pre-existing API calls.

Fact

Removed, Not Docker Itself

Docker Engine, Docker CLI, and Docker Compose remained fully functional outside of Kubernetes after this change.

Fact

A Long-Planned Change

The deprecation was formally announced roughly a year before its removal in Kubernetes 1.24, giving operators time to migrate.

2Why a Shim Existed At All: The Origins of CRI

To understand why dockershim was ever created, and why it was eventually removed, it helps to understand the problem CRI was designed to solve in the first place.

In Kubernetes’s earliest years, its code that talked to a container runtime was written specifically, directly, and exclusively for Docker. There was no abstraction layer — Kubernetes’s core scheduling and orchestration logic was tightly interwoven with Docker-specific API calls. This worked well while Docker was effectively the only serious container runtime in widespread use, but it created a real problem as alternative runtimes — like rkt (an early, now-retired competitor) and later containerd and CRI-O — began to emerge, each offering genuine advantages for certain use cases.

Every time Kubernetes wanted to support a new runtime, its maintainers had to write and maintain entirely separate, deeply embedded integration code specific to that runtime, directly inside Kubernetes’s own core codebase. This was expensive to maintain, made the core codebase harder to reason about, and effectively meant runtime maintainers had to convince Kubernetes’s own maintainers to accept and maintain runtime-specific code on their behalf — a significant coordination burden for both sides.

i
Key Idea

The Container Runtime Interface (CRI) was introduced specifically to fix this. Rather than Kubernetes containing custom code for every runtime, CRI defines one standard, stable, gRPC-based API contract. Any container runtime that implements this one contract can work with any version of Kubernetes that supports CRI, without Kubernetes’s own maintainers needing to write or maintain any runtime-specific code at all. The runtime maintainers, not the Kubernetes maintainers, now own the responsibility of speaking CRI correctly.

This is precisely the gap dockershim was built to paper over during the transition. When CRI was introduced, Docker Engine itself did not implement CRI natively — it had its own separate API, built years earlier for entirely different purposes, and there was no realistic expectation that Docker’s own maintainers (a separate company, with their own priorities) would immediately rewrite Docker Engine to natively speak this new Kubernetes-specific interface. So, rather than break compatibility with the most widely used container runtime in the ecosystem overnight, Kubernetes’s own maintainers wrote and maintained dockershim themselves, as a temporary, Kubernetes-side bridge specifically so existing Docker-based clusters could keep working uninterrupted while CRI matured and while genuinely CRI-native runtimes became production-ready alternatives.

It’s worth appreciating just how large a share of the ecosystem dockershim needed to support during this transitional period. At the time CRI was introduced, Docker was not simply one option among several — it was, by a wide margin, the default assumption most Kubernetes users, tutorials, and third-party tooling had already built around. Removing Docker support immediately, the moment CRI was defined, would have broken a very large number of existing production clusters overnight, with no reasonable migration path available yet, since containerd and CRI-O were not yet mature, independently packaged, CRI-native options at that early stage. Dockershim’s real value, in this light, was buying the ecosystem time — letting CRI-native runtimes mature into genuinely production-ready alternatives while the overwhelming majority of existing Docker-based clusters kept working exactly as they always had, with no forced, premature migration.

EraHow Kubernetes Talked to Runtimes
Early KubernetesHardcoded, Docker-specific integration built directly into Kubernetes’s core code
CRI introducedA standardized interface defined; Docker didn’t natively support it yet
Dockershim eraKubernetes maintains a translation layer so Docker can still be used via CRI-shaped calls
Post-deprecationKubernetes only supports runtimes that natively implement CRI directly, no translation layer included

3Docker’s Own Internal Layers

A detail that surprises many people learning this topic for the first time: Docker Engine itself is not one single monolithic program — it’s built from several layered components, and one of those components is the very same technology many Docker alternatives use directly.

When you run a container with the Docker CLI, the request passes through several distinct layers before a container actually starts. The Docker CLI is what a user types commands into. It talks to the Docker daemon (dockerd), a long-running background process that handles image management, networking, volumes, and the overall Docker API surface most tools integrate with. The Docker daemon, in turn, delegates the actual work of running containers to containerd, a separate, more focused container runtime that Docker itself originally created and later donated to the Cloud Native Computing Foundation as its own independent open-source project. Containerd itself delegates the lowest-level work — actually creating the isolated process using kernel features like namespaces and cgroups — to runc, a minimal, low-level tool implementing the Open Container Initiative (OCI) runtime specification.

flowchart TB
    CLI["Docker CLI"] --> D["Docker Daemon (dockerd)"]
    D --> C["containerd"]
    C --> R["runc"]
    R --> K["Linux Kernel\n(namespaces, cgroups)"]
    style D fill:#111,stroke:#dc2626,color:#fff
    

Fig. 1 — Docker’s own internal chain: the CLI talks to dockerd, which delegates to containerd, which delegates to runc, which finally talks to the kernel.

“The great irony of dockershim’s removal is that the runtime most clusters ended up switching to — containerd — was, for years, already quietly doing the real work underneath Docker itself.”

This layered architecture is central to understanding why the dockershim deprecation turned out to be far less disruptive in practice than its announcement initially made it sound. Because containerd already sat directly underneath Docker, doing the actual container-running work, switching a Kubernetes cluster’s configured runtime from “Docker” to “containerd directly” did not require replacing some entirely foreign, unfamiliar piece of technology — for most clusters, it meant removing one layer (Docker Engine and dockershim together) and letting Kubernetes talk to a component, containerd, that had effectively been doing the real work all along, just one hop further down the chain.

It’s worth pausing on why Docker was ever built this way in the first place, since it explains a great deal about the ecosystem’s later direction. In Docker’s earlier years, before containerd existed as a distinct, independent project, Docker Engine handled the entire chain — image management, networking, and low-level container execution — as one more tightly bundled piece of software. As container adoption grew well beyond Docker’s own original use cases, Docker’s own engineers recognized that other projects, including but not limited to Kubernetes, could benefit from reusing just the lower-level, execution-focused part of Docker’s own stack, without needing everything else Docker Engine bundled alongside it. Splitting that execution layer out into containerd, and subsequently donating it to the Cloud Native Computing Foundation as an independently governed project, was Docker’s own deliberate step toward exactly the kind of modular, interchangeable runtime ecosystem that CRI was later designed to standardize access to.

4What Dockershim Actually Was: Architecture & Components

A precise look at where dockershim physically lived and what it was responsible for.

Dockershim lived inside the kubelet — the Kubernetes agent that runs on every node in a cluster and is directly responsible for actually starting, stopping, and monitoring the containers that make up the Pods scheduled onto that node. Before dockershim’s removal, when the kubelet needed to perform a container operation, it made that request internally using CRI’s standard, gRPC-based method calls, exactly as it would for any CRI-compliant runtime. Dockershim intercepted those calls, on the kubelet’s own side, translated them into the equivalent Docker Engine API calls, sent those to the Docker daemon, and then translated Docker’s responses back into the shape CRI expected, handing that back to the rest of the kubelet’s logic.

  • CRI shim — the general term for any adapter component that lets a non-CRI-native runtime be used as if it were CRI-compliant; dockershim was the specific shim built for Docker.
  • Kubelet — the node-level Kubernetes agent that dockershim was embedded directly inside of, rather than existing as a separate, standalone process.
  • CRI (Container Runtime Interface) — the standardized gRPC API contract the kubelet expects any runtime (or shim acting on a runtime’s behalf) to implement.
  • Docker Engine API — Docker’s own, separate, pre-existing API that dockershim translated CRI calls into on the other side of the bridge.
Component

Image Management Translation

Dockershim translated CRI image-pulling and image-listing requests into Docker’s own image API calls, since Kubernetes needed a consistent way to manage images regardless of runtime.

Component

Container Lifecycle Translation

Create, start, stop, and remove operations issued by the kubelet in CRI’s format were translated into the equivalent Docker container commands.

Component

Pod “Sandbox” Emulation

Kubernetes’s CRI model expects a “pod sandbox” concept — a shared network namespace containers in a Pod join — which Docker didn’t natively understand the same way; dockershim had to emulate this using an extra, hidden “pause” container Docker did understand.

Component

Networking Plumbing (CNI Bridge)

Dockershim also handled invoking Container Network Interface (CNI) plugins to set up Pod networking, since Docker’s own built-in networking model didn’t natively align with how Kubernetes expected Pod networking to be configured.

!
A Subtlety Worth Knowing

That “pause” container mentioned above is not a dockershim-specific invention — it’s a broader Kubernetes concept (sometimes called the “pause” or “infra” container) used to hold open a Pod’s shared network namespace for the lifetime of the Pod, regardless of which runtime is in use. Dockershim’s specific job was making sure this concept, which CRI assumes natively, could still be correctly emulated using Docker’s own container primitives, which had no native “sandbox” concept of their own.

5Internal Working: How Dockershim Translated a Request

Zooming into the actual mechanics of the translation process, to see precisely why maintaining this bridge was a genuinely heavy, ongoing engineering burden.

Every CRI request the kubelet made was defined in terms of concepts CRI itself introduced — pods, sandboxes, standardized container states — while Docker’s own API was defined in terms of Docker’s own, older concepts, which didn’t always map cleanly one-to-one. Dockershim’s internal job was to sit in the middle of this mismatch and paper over every one of these conceptual gaps, consistently, correctly, for every single call, release after release.

1

CRI Call Received

The kubelet’s internal logic issues a standard CRI gRPC call, for example, “create this pod sandbox” — a concept native to CRI’s model of the world.

2

Dockershim Interprets the Request

Dockershim receives this CRI-shaped call and figures out what combination of Docker-native operations would produce an equivalent result — in this case, typically starting a special, minimal “pause” container to hold open the shared network namespace.

3

Docker Engine API Calls Issued

Dockershim makes the actual Docker Engine API calls needed, using Docker’s own vocabulary and data structures, which Docker Engine itself understands directly with no awareness that CRI exists at all.

4

Docker Responds in Its Own Format

Docker Engine returns a response shaped entirely around its own internal concepts and data structures — again, with no built-in knowledge of CRI’s expectations.

5

Dockershim Reshapes the Response

Dockershim converts Docker’s response back into the exact shape CRI defines, so the rest of the kubelet’s internal logic — which was written generically against CRI, not against Docker specifically — receives exactly what it expects, regardless of which runtime actually serviced the request underneath.

This translation had to happen correctly for every operation type CRI defines, and had to keep working correctly as both Kubernetes’s own CRI specification and Docker’s own independent API continued to evolve over time, on their own separate release schedules, maintained by two entirely different engineering organizations with no obligation to coordinate their changes with each other. That ongoing, dual-sided maintenance burden — keeping a translation layer correct between two independently moving targets — is the central engineering reality that eventually made the case for removing dockershim rather than continuing to maintain it indefinitely.

A good way to appreciate the scale of this burden is to consider what happens whenever either side of the translation changes independently. If a new Kubernetes release introduced a new CRI capability — say, a more precise way of reporting container resource usage — dockershim’s maintainers had to figure out whether, and how, that new capability could be expressed using Docker’s existing, unrelated API, which was never designed with that specific CRI feature in mind. Sometimes the mapping was straightforward; sometimes it required an awkward workaround, or wasn’t fully achievable at all, producing a subtle compatibility gap between what CRI formally promised and what a Docker-backed cluster could actually deliver. Symmetrically, if Docker’s own API changed in a new release — deprecating an old field, restructuring how a particular response was shaped — dockershim’s translation logic for that specific area had to be updated to match, or risk silently breaking. Neither Kubernetes’s release cycle nor Docker’s release cycle was designed around the other’s needs, which meant dockershim’s maintainers were perpetually reconciling two independently moving targets, indefinitely, for as long as the shim continued to exist.

6Data Flow & Lifecycle: Creating a Pod, Before and After

A side-by-side walkthrough of what happens when Kubernetes schedules a new Pod onto a node, comparing the dockershim-based path to the direct, post-deprecation path.

StepWith Dockershim (Pre-1.24)Without Dockershim (Post-1.24, e.g. containerd)
1Kubelet receives Pod spec from the API serverKubelet receives Pod spec from the API server
2Kubelet issues a CRI call to its embedded dockershimKubelet issues a CRI call directly to the configured CRI-native runtime
3Dockershim translates the call into Docker Engine API callscontainerd (or CRI-O) receives and processes the call natively, no translation needed
4Docker daemon processes the request, delegating internally to its own containerd instancecontainerd processes the request directly
5Response travels back up through Docker, then through dockershim’s translation, then to the kubeletResponse travels back directly to the kubelet, no intermediate translation layer

Notice the structural difference this table makes visible: in the dockershim path, a request from the kubelet actually passed through two separate containerd-adjacent layers on its way to a container actually starting — first Kubernetes’s own dockershim translation, then Docker Engine’s internal delegation to its own bundled containerd. In the post-deprecation path, that request goes directly to containerd in a single hop, with one fewer translation layer for the request (and every response) to pass through.

flowchart LR
    subgraph Before["Before: With Dockershim"]
      K1["Kubelet"] --> DS["Dockershim\n(CRI translation)"]
      DS --> DD["Docker Daemon"]
      DD --> C1["containerd"]
      C1 --> R1["runc"]
    end
    subgraph After["After: Direct CRI"]
      K2["Kubelet"] --> C2["containerd\n(native CRI)"]
      C2 --> R2["runc"]
    end
    style DS fill:#111,stroke:#dc2626,color:#fff
    

Fig. 2 — The dockershim path had an extra translation hop (and an extra bundled containerd instance) compared to the direct CRI path most clusters moved to.

This is also, practically speaking, why most workloads themselves noticed essentially no difference after migration: from an application container’s own point of view, it is still ultimately started by runc, still isolated by the same kernel namespaces and cgroups, and still scheduled by the same Kubernetes control plane. The change was almost entirely confined to the node-level plumbing connecting the kubelet to the runtime — not to how the application itself actually ran once started.

It’s also worth tracing what happened to that extra “pause” container concept discussed in the previous chapter, since it’s a detail that occasionally surprised operators inspecting their nodes closely during migration. Under dockershim, each Pod’s sandbox was represented as an actual, visible Docker container — the pause container — sitting alongside the Pod’s real application containers when inspected with Docker’s own tooling. Under a direct CRI-native runtime like containerd, the equivalent sandbox concept is typically represented more natively within the runtime’s own internal model, without necessarily appearing as a separate, independently listed container in quite the same way Docker’s own tooling used to show it. This is a cosmetic, tooling-visible difference rather than a functional one — the underlying purpose (holding open a shared network namespace for the Pod’s lifetime) remains identical — but it’s exactly the kind of small, easy-to-notice change that prompted operators to double-check they hadn’t broken something, when in fact everything was working precisely as intended.

7Why Dockershim Was Deprecated

The Kubernetes project publicly documented several specific, concrete reasons behind this decision — it wasn’t an arbitrary or purely political choice.

Reason

Ongoing Maintenance Burden

As described in the previous chapter, dockershim required continuous, dedicated engineering effort from Kubernetes maintainers to keep working correctly as both Kubernetes and Docker independently evolved.

Reason

An Extra, Redundant Layer

Because Docker itself already delegated to containerd internally, running Kubernetes on top of Docker meant traffic passed through an extra, largely redundant layer of indirection that a direct containerd integration simply didn’t need.

Reason

Feature Lag & Compatibility Gaps

Certain newer CRI features and Kubernetes capabilities were harder, or sometimes impossible, to correctly express through the dockershim translation layer, creating an ongoing source of subtle compatibility gaps and bugs.

Reason

A More Sustainable Ecosystem

Encouraging container runtimes to implement CRI natively, rather than relying on Kubernetes-maintained shims, was seen as a healthier long-term architecture — putting responsibility for each runtime’s Kubernetes compatibility with the runtime’s own maintainers, not with Kubernetes’s core team.

i
A Notable Detail

It’s worth being fair to Docker’s own maintainers here: rather than leaving the ecosystem stranded, Docker (specifically Mirantis, which maintains the Moby project that Docker Engine is built from) released a separate, standalone, genuinely CRI-native shim called cri-dockerd, maintained outside of the core Kubernetes codebase, specifically so organizations that wanted to keep using Docker Engine underneath Kubernetes for whatever reason could still do so — just via a shim maintained by the Docker ecosystem itself, rather than one bundled inside Kubernetes.

It’s also worth directly addressing a common misconception from when this announcement first circulated: the deprecation was not a statement that Docker Engine was somehow inferior, insecure, or unreliable as a piece of software. It was a statement, specific to the Kubernetes project’s own architecture and maintenance priorities, that maintaining a Kubernetes-side translation layer for one specific, non-CRI-native runtime was no longer the sustainable long-term approach, now that genuinely CRI-native alternatives — chief among them containerd, which Docker itself had already built and donated to the ecosystem — were mature and production-ready.

There is also a useful open-source governance lesson embedded in this story, worth calling out on its own. Kubernetes and Docker, despite both being central, closely associated pillars of the same broader container ecosystem, are governed by entirely separate organizations, with separate roadmaps, separate priorities, and separate release cadences. For years, dockershim quietly papered over the fact that these two separately governed projects had no formal obligation to keep their respective APIs in sync with each other. The deprecation decision was, in part, an acknowledgment that relying indefinitely on one open-source project’s maintainers to keep bridging gaps introduced by another, separately governed project’s own independent evolution was structurally fragile — a healthier long-term architecture put that bridging responsibility on whichever party controlled the runtime side of the equation, aligning ownership with capability rather than leaving it with a third party that had no direct control over either side of what it was translating between.

8The Migration Path: Moving Off Dockershim

The practical steps organizations running Docker-based Kubernetes clusters actually needed to take.

1

Identify Affected Nodes

Determine which nodes in a cluster were configured to use Docker (via dockershim) as their container runtime, since managed Kubernetes offerings and newer clusters were frequently already running containerd or CRI-O directly.

2

Audit for Docker-Specific Dependencies

Check for anything relying on Docker-specific behavior directly — for example, tooling that shelled out to the Docker socket, or workflows that assumed Docker-specific image storage details — since these needed separate attention regardless of which runtime replaced dockershim.

3

Choose a Replacement Runtime

Most organizations chose containerd, given its direct lineage from Docker and its already-proven role running the same workloads underneath Docker for years; CRI-O was the other major, genuinely CRI-native option, particularly common in Red Hat OpenShift-based environments.

4

Install & Configure the New Runtime

Install the chosen CRI-native runtime on each affected node and configure the kubelet to communicate with it directly via CRI, rather than through Docker and dockershim.

5

Test, Then Roll Out Gradually

Validate application behavior on a small subset of migrated nodes before rolling the change out cluster-wide, watching closely for any of the Docker-specific dependencies identified in the audit step.

!
Important Distinction

This migration was strictly about the container runtime configured on Kubernetes nodes. It had no bearing on developers continuing to use Docker Desktop, the Docker CLI, or docker build on their own laptops to build images — those workflows remained entirely unaffected, since they don’t involve the kubelet or dockershim at all. The confusion between “Docker as a development tool” and “Docker Engine as a Kubernetes node’s container runtime” was, in practice, the single biggest source of unnecessary alarm when this change was first announced.

9containerd & CRI-O as Direct Alternatives

A closer look at the two runtimes that absorbed the vast majority of clusters migrating off dockershim.

AspectcontainerdCRI-O
OriginOriginally built by Docker, donated to the CNCFBuilt by Red Hat and community contributors, specifically for Kubernetes from the start
Design goalGeneral-purpose container runtime, usable both standalone and via CRIPurpose-built exclusively to be a lightweight, minimal CRI implementation for Kubernetes
Relationship to DockerWas already running underneath Docker Engine for yearsEntirely independent of Docker’s own internals
CRI supportNative, direct CRI plugin built inNative, since CRI compliance was its founding purpose
Common inMost major managed Kubernetes offerings by defaultCommonly the default in Red Hat OpenShift and related distributions

Both runtimes speak CRI natively, meaning neither requires any Kubernetes-maintained translation layer at all — the kubelet talks to them directly, using CRI’s standard gRPC interface, exactly as CRI’s designers originally intended every runtime to eventually support. Choosing between them is typically driven less by raw capability differences, since both are mature, production-proven options, and more by an organization’s existing ecosystem — for example, which runtime their chosen Kubernetes distribution or managed cloud offering ships and supports by default.

Why containerd Became the Default Choice for Most Migrations

Because containerd had already been quietly running every Docker-based container for years, as the layer directly beneath Docker Engine itself, migrating a Docker-based cluster to containerd meant the underlying execution behavior — how containers were actually created, isolated, and run — barely changed at all. What changed was the layer of software sitting directly above the runtime, not the runtime doing the actual work, which is precisely why so many teams described this migration as far less disruptive than the initial deprecation announcement had led them to expect.

Beyond containerd and CRI-O, it’s worth knowing that the CRI-native runtime landscape has continued to broaden further, particularly around specialized isolation requirements. Runtimes like gVisor and Kata Containers, for example, implement the same CRI interface while providing meaningfully stronger workload isolation than a standard runc-based container — gVisor by intercepting and mediating system calls through its own user-space kernel, and Kata Containers by running each container inside its own lightweight, dedicated virtual machine rather than sharing the host kernel directly. Neither of these approaches existed as practical, CRI-compliant options for most organizations during Docker’s early dominance of the ecosystem, and their emergence is itself a direct product of the same architectural shift dockershim’s deprecation reinforced: once CRI became the universal contract every runtime needed to speak, the door opened for genuinely novel runtime implementations, optimized for very different trade-offs, to plug into Kubernetes on equal footing with more traditional options.

10Advantages, Disadvantages & Trade-offs

An honest look at what this architectural change actually cost, and what it genuinely improved, for the Kubernetes ecosystem.

Advantages of the Change

  • Removes an entire, ongoing translation-layer maintenance burden from the core Kubernetes project
  • Eliminates a redundant extra hop for every single container lifecycle operation, cluster-wide
  • Aligns the ecosystem around a single, standardized interface (CRI), rather than one runtime receiving special, hardcoded treatment
  • Reduces the surface area for a whole class of compatibility bugs specific to the translation layer itself
  • Encourages runtime maintainers, not Kubernetes’s core team, to own their own Kubernetes compatibility going forward

Disadvantages & Costs

  • Required a genuine, non-trivial migration effort for every organization still running Docker-based clusters at the time
  • Created short-term confusion, given how easily “Docker Engine as a Kubernetes runtime” and “Docker as a development tool” were conflated
  • Tooling or workflows with hardcoded, Docker-specific assumptions needed separate remediation, not just a runtime swap
  • Teams with limited operational bandwidth had to prioritize an unplanned migration project alongside their existing roadmap

On balance, this trade-off is widely regarded within the Kubernetes community as a healthy, necessary piece of technical debt repayment — a temporary bridge, explicitly announced as temporary from very early on, being retired once its purpose (giving genuinely CRI-native runtimes time to mature) had been fully served. The disruption was real but time-bounded and well-telegraphed; the maintenance savings and architectural cleanliness it produced are ongoing and permanent.

It’s worth framing this trade-off using a distinction familiar from broader software engineering practice: the difference between a one-time migration cost and a recurring maintenance cost. Every organization that migrated off dockershim paid a bounded, one-time cost — the audit, the testing, the tooling updates described earlier in this guide — that, once complete, was simply finished. Kubernetes’s own maintainers, by contrast, had been paying dockershim’s maintenance cost repeatedly, release after release, for as long as the shim continued to exist, with no natural endpoint in sight until the deprecation decision created one deliberately. Viewed this way, the decision effectively converted an unbounded, indefinitely recurring cost borne centrally by the Kubernetes project into a bounded, one-time cost distributed across the many individual organizations still relying on the shim — a trade generally considered favorable for the ecosystem’s long-term health, even though it understandably felt like an unwelcome, unplanned expense to any individual team that had to absorb its share of that one-time cost on a timeline not of their own choosing.

1.24
KUBERNETES VERSION
THAT REMOVED IT
~1yr
ADVANCE NOTICE
BEFORE REMOVAL
1
FEWER TRANSLATION
LAYER, PER REQUEST

11Security Considerations

While primarily an architectural and maintenance story, this change carries a few genuine security implications worth understanding.

Removing an entire translation layer, by definition, removes whatever bugs — including potential security-relevant ones — that layer could have introduced or mishandled. A translation layer sitting between two independently evolving APIs is inherently a place where subtle mismatches can occur, and while dockershim was a mature, well-tested piece of code by the time of its removal, fewer moving parts in the critical path between the kubelet and the running container is generally a security improvement in its own right, independent of any specific vulnerability history.

i
A Practical Security Angle

Post-migration, security tooling and policy that directly inspects or restricts container-runtime-level behavior — for instance, tools that hook into the runtime’s own socket or API to enforce policy — need to correctly target whichever CRI-native runtime a cluster now actually uses (containerd or CRI-O), rather than assuming Docker’s socket or API is present. This is a straightforward but easy-to-overlook detail during migration: any security tooling with a hardcoded assumption about Docker’s specific API surface needs its own explicit review and, where necessary, reconfiguration.

It’s also worth noting that neither the removal of dockershim, nor the choice between containerd and CRI-O, meaningfully changes the deeper security fundamentals covered elsewhere in container security discussions — namespaces, cgroups, mandatory access control systems like AppArmor or SELinux, and seccomp filtering all continue to operate at the kernel level, beneath whichever specific runtime is in use, largely unaffected by this particular architectural change. Dockershim’s removal changed how the kubelet talks to a runtime; it did not change how that runtime, in turn, talks to the kernel to actually isolate a container.

One area genuinely worth a closer look during migration, however, is the socket or API endpoint a node’s various pieces of tooling authenticate against and are granted access to. Docker’s own socket, historically, was a frequent target of security hardening discussions precisely because anything with access to it effectively had broad control over every container on that host. Migrating to a different runtime doesn’t eliminate this category of risk — containerd and CRI-O expose their own equivalent sockets and APIs, which carry comparably significant privilege if left insufficiently restricted. The practical security task during migration is less “is the new runtime inherently safer” and more “have all the same access-control and least-privilege principles previously applied to the Docker socket been correctly reapplied to whichever new socket or API surface replaced it” — a migration is a natural, useful checkpoint to revisit and re-verify this kind of access policy, rather than simply assuming prior hardening carried over automatically.

12Observability Impact: What Changes for Monitoring & Debugging

Operators accustomed to Docker-specific tooling and troubleshooting habits needed to adjust some of their day-to-day debugging workflows after migration.

Commands and tools that directly relied on the Docker CLI or the Docker Engine API to inspect running containers on a Kubernetes node — for example, running docker ps directly on a node to see what’s running — stopped reflecting reality once that node’s runtime was no longer Docker, since there was no longer a Docker daemon present on the node to query in the first place. Operators needed to adopt the equivalent tooling for whichever new runtime was in place — crictl, a CRI-focused command-line tool, is the direct replacement most commonly used across both containerd and CRI-O, offering similar inspection capabilities without being tied to any single runtime’s proprietary API.

Old Habit (Docker-Specific)New Approach (Runtime-Agnostic)
docker ps on a nodecrictl ps
docker logs <container>crictl logs <container>, or standard Kubernetes-level kubectl logs
docker inspectcrictl inspect
Docker daemon logscontainerd or CRI-O’s own service logs, via the host’s standard logging system

Why This Mattered More Than It First Appeared

Many organizations had, often informally, built up years of tribal knowledge and internal runbooks around Docker-specific debugging commands for diagnosing node-level container issues. Migrating away from dockershim meant this operational knowledge needed a deliberate refresh — not because the underlying troubleshooting concepts changed, but because the exact commands operators reached for during an incident needed to be updated, and ideally documented and rehearsed, before an actual production incident made that gap in muscle memory obvious at the worst possible time.

At the level Kubernetes users interact with most often — kubectl logs, kubectl exec, kubectl describe pod — essentially nothing changed at all, since these commands operate through the Kubernetes API server and kubelet, not by talking to the underlying runtime’s own tooling directly. The observability impact of this migration was overwhelmingly concentrated in lower-level, node-focused debugging workflows, not in everyday application-level Kubernetes usage.

Third-party observability and security platforms integrated with container infrastructure also had their own share of adjustment to make. Products that shipped a Docker-specific data collection agent — one that, for instance, watched the Docker daemon’s own event stream to detect container starts and stops — needed a CRI-compatible or containerd-specific equivalent to keep functioning on migrated nodes. Most established observability vendors had already anticipated this shift well ahead of the Kubernetes 1.24 release, given how publicly telegraphed the deprecation was, and released CRI-compatible or containerd-native collection agents in good time; but organizations running older versions of third-party tooling, or homegrown collectors built in-house without anyone tracking this specific upstream change, occasionally discovered gaps in their own observability coverage only after a cluster had already been migrated — a strong practical argument for including third-party tooling explicitly in the pre-migration dependency audit described earlier in this guide, rather than assuming vendor tooling would simply keep working unattended.

13Design Patterns & Anti-Patterns

Lessons worth carrying forward, both about this specific migration and about managing dependencies on Kubernetes’s own evolving internals more broadly.

Pattern

Treat Deprecation Notices as Actionable, Early

Kubernetes’s deprecation policy gives advance notice specifically so organizations can plan migrations calmly; treating an early deprecation announcement as a scheduled project rather than an eventual emergency avoids last-minute scrambling.

Pattern

Avoid Hardcoding Assumptions About the Underlying Runtime

Tooling, scripts, and monitoring built against a specific runtime’s proprietary API, rather than against the standardized CRI interface or Kubernetes’s own API, is inherently more fragile to exactly this kind of underlying architectural change.

Pattern

Test Runtime Migrations on Non-Production Clusters First

Validating application behavior, custom tooling, and monitoring on a staging or canary cluster before touching production nodes catches Docker-specific dependencies while the stakes are low.

Pattern

Separate “Development Tooling” From “Cluster Runtime” in Planning

Explicitly distinguishing Docker’s role as a developer-facing image-building tool from its (now-retired) role as a Kubernetes node runtime avoids the confusion that caused much of the original public concern around this change.

ANTI-01 Anti-Pattern
Problem

Ignoring the deprecation announcement until the Kubernetes version that actually removed dockershim was already being adopted, or was unavoidable due to a managed platform’s own upgrade schedule.

Why It Hurts

This converts a calm, well-planned migration with a year of lead time into a rushed, high-pressure project undertaken under deadline pressure, increasing the risk of overlooked Docker-specific dependencies causing production incidents.

Better Approach

Treat major container-runtime deprecation notices from Kubernetes as immediate planning triggers, scheduling the audit and migration work well ahead of any forced upgrade deadline.

ANTI-02 Anti-Pattern
Problem

Assuming that removing Docker from Kubernetes nodes also meant developers had to stop using Docker Desktop or the Docker CLI for local development and image building.

Why It Hurts

This misunderstanding led some teams to undertake unnecessary, disruptive changes to entirely unrelated developer workflows that were never actually affected by the Kubernetes-side change in the first place.

Better Approach

Clearly communicate, early in any migration planning, that this change is scoped to node-level container runtimes only, and has no bearing on local development tooling built around Docker.

14Best Practices & Common Mistakes

Practical guidance drawn from how organizations successfully navigated this migration in real production Kubernetes environments.

Best Practice

Check Your Managed Platform’s Timeline First

Many managed Kubernetes offerings handled this migration on behalf of customers, or provided a clear, documented timeline for it; checking the specific platform’s own guidance before building an independent migration plan avoids duplicated or conflicting effort.

Best Practice

Inventory Tooling Before Choosing a Replacement Runtime

A thorough audit of scripts, monitoring integrations, and security tools that assume Docker’s specific API surface, completed before the runtime swap, surfaces the real scope of work far more accurately than discovering gaps reactively after migration.

Best Practice

Update Runbooks and Train the On-Call Team

Since the most common operational friction after migration involved node-level debugging habits, updating incident runbooks and briefing the on-call rotation on the new tooling (like crictl) before an actual incident pays off directly during the next one.

Best Practice

Prefer containerd Unless a Specific Reason Points to CRI-O

Given containerd’s direct lineage from Docker’s own internals and its status as the default across most managed offerings, it represents the path of least resistance for most Docker-based migrations, absent a specific reason (such as an OpenShift-based platform) to choose CRI-O instead.

Common MistakeConsequenceFix
Conflating Docker Desktop usage with Kubernetes node runtime usageUnnecessary disruption to unrelated developer workflowsClearly scope the migration to node-level Kubernetes configuration only
Not auditing for Docker-socket-dependent tooling before migratingMonitoring or security tools silently stop working after the runtime swapInventory and test all node-level tooling against the new runtime before rollout
Migrating all nodes simultaneously without a canary phaseA previously unnoticed dependency causes a cluster-wide incident instead of a contained oneRoll out the runtime change gradually, starting with a small subset of nodes
Leaving old Docker-specific debugging runbooks unchangedOn-call engineers reach for commands that no longer work during a real incidentUpdate and rehearse runbooks against the new runtime’s tooling ahead of time

15Real-World & Industry Examples

This deprecation was one of the most widely discussed changes in recent Kubernetes history, precisely because it touched such a large share of existing production clusters.

Major Cloud Providers’ Managed Kubernetes Offerings

The large managed Kubernetes platforms operated by major cloud providers proactively communicated migration timelines to customers well ahead of the Kubernetes 1.24 release, and in many cases began defaulting new clusters to containerd long before the formal deprecation, specifically to reduce the number of customers who would need to actively migrate later.

Enterprise Platforms Built on Docker-Centric Tooling

Organizations that had built substantial internal tooling directly around Docker’s own API — custom deployment scripts, internal dashboards querying the Docker socket directly, homegrown security scanning hooked into Docker events — represented the segment of the ecosystem most affected by this change, since their remediation work extended well beyond simply swapping the configured runtime.

Kubernetes Distributions Built Around CRI-O

Platforms like Red Hat OpenShift, which had already standardized on CRI-O as their default container runtime well before dockershim’s deprecation was even announced, experienced essentially no disruption from this specific change, since their clusters were never using dockershim to begin with — a useful reminder that by the time of removal, a substantial share of the ecosystem had already moved on independently.

The Broader CNCF Ecosystem’s Response

The Cloud Native Computing Foundation’s wider community treated this deprecation as a validating moment for the original architectural vision behind CRI itself — proof that a standardized interface could successfully allow the ecosystem to consolidate around fewer, more focused runtime implementations, rather than requiring every orchestrator to maintain bespoke, runtime-specific integration code indefinitely.

The public communication effort around this specific deprecation is also frequently cited, within the Kubernetes community itself, as a reference example for how a large, widely used open-source project can retire a significant piece of legacy infrastructure responsibly. The combination of an early, clearly dated announcement; detailed, freely available migration documentation; direct outreach and default-behavior changes from major managed platform providers; and a genuinely less disruptive technical reality than the initial announcement’s headline suggested, together meant that by the time Kubernetes 1.24 actually shipped, a substantial share of the ecosystem had already completed its migration calmly, well ahead of any forced deadline — turning what could have been portrayed as an abrupt, disruptive breaking change into a comparatively smooth, well-managed architectural transition.

16Frequently Asked Questions

Q1Does dockershim deprecation mean I can no longer use Docker at all?

No. It only means Kubernetes no longer includes a built-in translation layer for using Docker Engine directly as a cluster’s container runtime. Docker itself — the CLI, the daemon, Docker Compose, Docker Desktop — remains fully functional for local development, image building, and standalone container use, completely independent of this Kubernetes-specific change.

Q2My images are built with Docker — do those still work on a cluster that no longer uses dockershim?

Yes. Docker builds standard, OCI-compliant container images, and both containerd and CRI-O can run OCI-compliant images without any issue, regardless of what tool built them. This is one of the most common points of confusion — the image format itself was never tied to dockershim at all.

Q3What if I genuinely still need to use Docker Engine as my Kubernetes runtime?

The community-maintained cri-dockerd project, maintained by Mirantis outside of the core Kubernetes codebase, provides a standalone, genuinely CRI-compliant shim for Docker Engine, letting organizations with a specific reason to keep using Docker Engine on their nodes continue to do so, just via a differently maintained bridge than the one Kubernetes used to bundle internally.

Q4Which Kubernetes version actually removed dockershim?

Dockershim was removed from the Kubernetes codebase starting with the Kubernetes 1.24 release, following a deprecation announcement made roughly a year earlier to give the ecosystem time to plan and execute migrations.

Q5Do I need to change my Kubernetes YAML manifests or application code because of this?

No. This change is entirely confined to node-level infrastructure configuration — which container runtime the kubelet talks to. It has no effect on Pod specifications, Deployments, Services, or any other Kubernetes API object an application team would normally write or maintain.

Q6How can I tell whether my current cluster is affected by this at all?

Checking the container runtime reported for each node (available through standard Kubernetes node-inspection commands, which report the runtime and its version directly) reveals immediately whether a cluster is running Docker, containerd, CRI-O, or another CRI-compliant runtime — clusters already on containerd or CRI-O were never depending on dockershim in the first place and require no migration.

Q7Did this change affect Docker Swarm as well?

No. Docker Swarm is Docker’s own separate, built-in clustering and orchestration system, entirely independent of Kubernetes. Dockershim existed specifically as a bridge between Kubernetes’s kubelet and Docker Engine; Swarm doesn’t use the kubelet or CRI at all, so this deprecation has no bearing on Swarm-based deployments whatsoever.

Q8Is containerd itself at risk of a similar deprecation someday?

Containerd is a native CRI implementation, maintained specifically with Kubernetes compatibility as one of its core goals, and it’s an independently governed CNCF project used by a large share of the Kubernetes ecosystem. The structural problem that led to dockershim’s removal — a Kubernetes-maintained translation layer bridging to a non-CRI-native external API — simply doesn’t apply to containerd, since it speaks CRI directly rather than requiring Kubernetes’s own maintainers to translate on its behalf.

17Summary & Key Takeaways

What to Remember

  • Dockershim was a Kubernetes-maintained translation layer, living inside the kubelet, that let Docker Engine — which never natively spoke Kubernetes’s standardized Container Runtime Interface (CRI) — be used as a Kubernetes node’s container runtime.
  • Its removal, finalized in Kubernetes 1.24, did not deprecate or remove Docker itself — Docker Engine, the Docker CLI, Docker Compose, and Docker Desktop all remained fully functional for local development and standalone use, entirely independent of this Kubernetes-specific architectural change.
  • The core motivation was a genuine, ongoing engineering burden: maintaining a translation layer between two independently evolving APIs (CRI and Docker’s own API) indefinitely was less sustainable than requiring runtimes to implement CRI natively.
  • containerd — already running underneath Docker Engine for years — absorbed the majority of migrations, meaning the actual, lowest-level container execution behavior for most workloads barely changed at all.
  • Images built with Docker remained fully compatible, since Docker produces standard OCI images that any CRI-compliant runtime can run without modification.
  • The migration’s real-world impact was concentrated in node-level tooling and operational habits — debugging commands, monitoring integrations, and runbooks built around Docker’s specific API — rather than in application-level Kubernetes usage, which was largely unaffected.
  • For organizations with a specific, ongoing need to keep using Docker Engine on Kubernetes nodes, the community-maintained cri-dockerd project offers a supported path forward, separate from the shim Kubernetes used to bundle internally.
  • Treat future deprecation announcements the same way: as an early, actionable planning signal rather than a distant concern, since the organizations that migrated calmly were consistently the ones that started well before the removal deadline rather than waiting for it to become unavoidable.