不少刚接触容器技术的同学都会有一个困惑:containerd 和 Docker 到底是什么关系?为什么装了 Docker 之后会看到 containerd 的进程,而在 Kubernetes 集群里又常常被建议直接使用 containerd 而不装 Docker?要回答这些问题,得从容器技术这几年的架构演进说起。简单来讲,Docker 是一套面向开发者的完整容器工具集,而 containerd 是从 Docker 中拆分出来、专门负责容器生命周期管理的一个工业级运行时,两者不是竞争关系,而是上下游的分工协作关系。

一、从架构演进看两者的渊源
Docker 早期的架构非常简单:一个 monolithic 的 docker daemon 包揽所有事情,包括镜像构建、镜像分发、容器网络、容器存储、容器生命周期管理等。这种设计在初期跑得很顺,但随着容器生态的膨胀,问题逐渐暴露。一方面 Kubernetes 这类编排系统只需要「创建容器、管理容器」这个核心能力,不需要 Docker 的构建和 CLI 体验;另一方面所有功能堆在一个 daemon 里,任何一个模块出问题都可能拖垮整个服务。
于是 Docker 在 2016 年底把容器管理核心拆了出来,捐赠给 CNCF,这就是 containerd 的由来。拆分后的链路变成了:docker CLI 把请求发给 dockerd,dockerd 处理构建、网络、卷等上层逻辑,真正启动容器时调用 containerd,containerd 再调用更底层的 runc 去创建容器进程。runc 是 OCI 运行时标准的参考实现,负责和内核的 namespace、cgroup 打交道,是真正「生出」容器的那一层。
后来 containerd 独立发展,2019 年从 CNCF 正式毕业,如今已经是不依赖 Docker 也能独立部署的通用容器运行时。所以准确地说:Docker 内置了 containerd,但 containerd 不属于 Docker。
二、职责边界:Docker 做了哪些 containerd 不做的事
两者的核心差异在于功能覆盖面。Docker 在 containerd 之上补齐了大量面向开发者体验的能力,比如 docker build 镜像构建、docker-compose 编排、docker network 提供的 bridge 网络和端口映射、数据卷管理,以及一套非常好用的命令行工具。这些能力让 Docker 成为本地开发和持续集成场景的事实标准。
而 containerd 只关注容器运行时最核心的部分:镜像的拉取与存储、容器生命周期的管理、快照管理、任务监控等。它刻意不做构建、不做复杂网络、不做卷的高级抽象,保持小而稳。这种克制恰恰是 Kubernetes 需要的——编排系统希望自己掌控网络、存储和调度,运行时只需要老老实实把容器跑起来就行。
在容器的层级结构上可以这样理解:
docker CLI # 用户交互入口
└── dockerd # 处理构建、网络、卷等上层逻辑
└── containerd # 容器生命周期管理、镜像管理
└── containerd-shim # 容器与守护进程解耦的中间层
└── runc # 真正创建容器进程(OCI 运行时)其中 containerd-shim 值得一提:它让每个容器对应一个独立的 shim 进程,即使 containerd 重启或崩溃,正在运行的容器也不会被杀死。dockerd 不再直接管理容器进程,这正是当年架构拆分带来的最大好处之一。
三、Kubernetes 场景下的选择:为什么要弃用 dockershim
熟悉 Kubernetes 的同学应该知道,Kubernetes 从 1.24 版本开始移除了内置的 dockershim 组件。这并不意味着 Docker 构建的镜像不能用了,而是说 kubelet 不再通过 Docker 这条链路来管理容器。
在这之前,kubelet 通过 CRI(容器运行时接口)和容器运行时通信。但 Docker 早期并没有实现 CRI,于是 Kubernetes 在 kubelet 里塞了一个 dockershim 做协议转换:CRI 请求先转成 Docker API 调用 dockerd,dockerd 再转给 containerd。一条链路上堆了两层转换,既低效又难维护。而 containerd 从 1.1 版本起原生实现了 CRI 插件,kubelet 可以直接对接 containerd,中间环节全部省掉。
两条链路的对比大致是:
# 旧链路(已废弃) kubelet -> dockershim -> dockerd -> containerd -> runc # 现行链路 kubelet -> CRI 插件 -> containerd -> runc
切换到 containerd 后,日常操作命令也发生了变化。比如查看容器不再用 docker ps,而是使用官方提供的 nerdctl 工具,或者 crictl:
# 使用 crictl 查看运行中的容器 crictl ps # 使用 nerdctl 拉取镜像并运行容器,体验接近 docker nerdctl pull nginx:latest nerdctl run -d --name web -p 8080:80 nginx:latest # 查看 containerd 的命名空间,k8s 的容器在 k8s.io 命名空间下 ctr namespaces list ctr -n k8s.io containers list
需要特别说明的是,containerd 引入了 namespace(命名空间)的概念来隔离不同客户端的镜像和容器。Docker 使用的镜像默认放在 moby 命名空间下,而 Kubernetes 放在 k8s.io 命名空间下。这就是为什么在一台机器上同时装了 Docker 和 containerd 时,用 docker images 看到的镜像和 crictl images 看到的可能完全不同,它们物理上都在 containerd 的存储里,但逻辑上互不可见。
四、实际场景中该如何选择
如果你是应用开发者,日常需要在本地构建镜像、调试服务、跑 docker-compose,那么继续用 Docker 是最省心的选择,Docker Desktop 一装全都有。此时 containerd 会在后台默默工作,你不需要直接和它打交道。
如果你是运维或平台工程师,负责 Kubernetes 集群,那么直接部署 containerd 是当前的主流方案。更少的中间层意味着更少的故障点和资源开销,集群节点上少跑一个 dockerd 进程,内存占用和攻击面都会降低。生产环境中也可以配合 ctr、nerdctl 这类工具完成日常的镜像排查和容器诊断。
总结成一句话:Docker 是给「人」用的容器工具套件,containerd 是给「平台」用的容器运行时。Docker 底下跑着 containerd,Kubernetes 直接跑在 containerd 上,两者共享同一套 OCI 镜像标准和 runc 运行时。理解了这条分层脉络,再看容器生态里的各种组件,就不会再混淆了。
containerdDocker容器运行时修改时间:2026-09-03 06:26:34