Docker 之下的那一层:containerd 是什么
你每天敲的 docker run,其实并不是「Docker 在跑容器」这么简单。真实的调用链是这样的:你敲的命令先被 Docker 客户端(CLI)接收,转成 API 请求发给 Docker 守护进程 dockerd;dockerd 负责镜像管理、网络、卷这些「高层」逻辑;而真正去拉镜像、创建容器进程、挂载文件系统这些「底层」动作,是委托给 containerd 完成的。containerd 再往下调用 runc,由 runc 通过 Linux 的 namespace 和 cgroup 真正把容器进程启动起来。
换句话说,containerd 才是容器生命周期的实际管理者。dockerd 是套在它外面的一层「便利封装」。
那你可能会问:既然 Docker 用得好好的,为什么要直接碰 containerd?原因有几个:
- 更轻:containerd 没有 dockerd 那层额外的镜像构建、编排、插件体系,内存占用和启动开销都更小,对低配 VPS 友好;
- 更稳定:Kubernetes 从 1.24 起正式弃用 dockershim,改用 containerd 作为默认容器运行时——因为 Kubernetes 只需要「运行时」,不需要 Docker 那一整套;
- 更透明:直接操作 containerd,你能更清楚地看到容器、镜像、命名空间是怎么组织的,排障时不再隔着一层黑盒;
- 避免「套娃」:有人喜欢在 Docker 容器里跑 Docker,这种做法问题很多,正确姿势之一就是直接在宿主机用 containerd。
好消息是,你不需要重新学一套复杂的东西。containerd 有一个非常好用的命令行工具叫 nerdctl,它的命令语法几乎和 docker 一模一样,习惯 Docker 的人可以无缝切换。
安装 containerd 与 nerdctl
先用官方源装 containerd。Debian/Ubuntu 下:
apt update
apt install -y containerd
mkdir -p /etc/containerd
containerd config default > /etc/containerd/config.toml
systemctl enable --now containerd
systemctl status containerdcontainerd config default 会生成一份默认配置文件。这一步很重要,因为很多参数(比如 cgroup 驱动、镜像加速)都需要在这份配置里改。生成后建议改一个关键项:如果你希望容器能用 systemd 管理 cgroup 资源限制,把 SystemdCgroup 打开:
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
SystemdCgroup = true改完重启 systemctl restart containerd。
接着装 nerdctl。它是一个独立二进制,直接从 GitHub Release 下载即可:
NERDCTL_VERSION=2.0.3
curl -fsSL -o /tmp/nerdctl.tar.gz \
"https://github.com/containerd/nerdctl/releases/download/v${NERDCTL_VERSION}/nerdctl-${NERDCTL_VERSION}-linux-amd64.tar.gz"
tar -C /usr/local/bin -xzf /tmp/nerdctl.tar.gz nerdctl
nerdctl versionnerdctl 默认通过 containerd 的 socket(/run/containerd/containerd.sock)通信,装好后直接就能用,不需要额外配置。
nerdctl 常用命令:几乎和 docker 一样
如果你会 docker,那 nerdctl 基本是零学习成本。对照表如下:
docker run→nerdctl rundocker ps→nerdctl psdocker images→nerdctl imagesdocker pull / push→nerdctl pull / pushdocker exec -it→nerdctl exec -itdocker logs -f→nerdctl logs -fdocker build→nerdctl build(需要另装 buildkit)
实战几个例子。跑一个 Nginx:
nerdctl run -d --name web -p 8080:80 nginx:alpine
nerdctl ps
nerdctl logs -f web注意这里的 -p 8080:80 能生效,是因为 nerdctl 内置了 CNI 网络插件支持;如果没装 CNI,端口映射会失败。装 CNI 很简单:
mkdir -p /opt/cni/bin
curl -fsSL https://github.com/containernetworking/plugins/releases/download/v1.5.1/cni-plugins-linux-amd64-v1.5.1.tgz \
| tar -C /opt/cni/bin -xz然后用 nerdctl compose 读 docker-compose.yml。它兼容大部分 Compose 语法:
nerdctl compose up -d
nerdctl compose ps
nerdctl compose down对个人站来说,这意味着一份现成的 compose 文件几乎不用改就能在 containerd 上跑起来。
命名空间:containerd 的隐藏维度
containerd 有一个 Docker 用户不太熟悉的概念——namespace(命名空间,简称 ns)。这里的 namespace 和 Linux 内核的 namespace 不是一回事,它是 containerd 用来隔离「镜像和容器集合」的逻辑空间。
Kubernetes 的容器跑在 k8s.io 这个 namespace 里,而 nerdctl 默认操作的是 default namespace。这就解释了一个常见现象:在 Kubernetes 节点上用 nerdctl ps 看不到任何容器——因为 K8s 的容器在 k8s.io 里,不是 default。要看它们得加 -n:
nerdctl -n k8s.io ps
nerdctl -n k8s.io images这个机制其实很有用:你可以用不同 namespace 把不同站点的容器隔离开,互不干扰地管理各自的镜像。比如给一个测试站单独开一个 ns:
nerdctl namespace create staging
nerdctl -n staging run -d --name web-test nginx:alpine这样 nerdctl ps 和 nerdctl -n staging ps 看到的是两批完全独立的容器,镜像也是分开存的(虽然底层共享相同的 blob,不会重复占磁盘)。
构建镜像:用 buildkit 而不是 docker build
Docker 引擎自带镜像构建能力,但 containerd 本身不做构建。要在 nerdctl 里 build,需要额外运行一个 buildkitd 服务:
nerdctl build --help # 会提示需要 buildkitd
# 用 nerdctl 直接拉起 buildkit 容器(最简单的方式)
nerdctl run -d --name buildkitd --privileged \
-v /run/containerd/containerd.sock:/run/containerd/containerd.sock \
moby/buildkit:latest然后指定 buildkit 的地址来构建:
nerdctl build --buildkit-host unix:///run/buildkit/buildkitd.sock -t myapp:v1 .或者用更省事的办法——在系统里装一个 rootless 的 buildkitd,通过 systemd 管理。这样就完全不依赖 Docker,纯 containerd 体系也能构建镜像。
这里有个实用技巧:nerdctl build 支持大多数 Dockerfile 指令,包括多阶段构建。这意味着你之前学的 Dockerfile 优化技巧(比如多阶段构建瘦身)可以原样迁移过来。
让容器开机能自启:生成 systemd 服务
Docker 有 --restart always,但 containerd 本身没有这个机制——它的设计哲学是「容器生命周期交给上层管理」。在 Kubernetes 场景下是 kubelet 在管,在纯 containerd 场景下,官方推荐用 systemd 来管。
nerdctl 提供了一个非常方便的功能,直接把正在运行的容器「导出」成一个 systemd 服务单元:
nerdctl generate systemd web > /etc/systemd/system/web.service
systemctl daemon-reload
systemctl enable --now web生成的 service 文件里,ExecStart 会是一个 nerdctl run 命令,还带了 --restart 相关的设置,由 systemd 负责在崩溃或重启后把容器拉起来。这样你就得到了一个不依赖 dockerd 的自启容器。
验证一下:systemctl status web 看到服务 active,nerdctl ps 看到容器在跑,重启机器后容器自动恢复,就成功了。
排查问题的实用命令
用 containerd 排障,除了 nerdctl,还要会用 ctr——containerd 自带的底层命令行工具。它比 nerdctl 更原始,但能看到更底层的东西:
# 列出所有 namespace
ctr namespace ls
# 看 default 命名空间下的容器
ctr -n default containers ls
# 看镜像
ctr -n default images ls
# 查看某个容器的任务(运行时状态)
ctr -n default tasks lsctr 的容器和 nerdctl 的容器是同一个东西,只是一边用 nerdctl 这个「友好封装」、一边用 ctr 这个「底层视图」。当 nerdctl 报错说不清楚时,用 ctr 能看到更详细的状态。
容器启动失败时,最有用的是查 containerd 的日志:
journalctl -u containerd -f以及看具体容器的退出状态和 OOM 记录。如果是容器被 OOM Killer 干掉,dmesg 里会有 Out of memory: Killed process 的记录,结合 nerdctl stats 看容器的内存使用曲线,就能判断是不是内存限制给少了。
五个必踩的坑
坑一:以为 nerdctl 和 docker 完全等价。命令语法像,但底层不同。最典型的是网络:docker 有内置的 bridge 网络和端口映射,nerdctl 依赖 CNI 插件,没装 CNI 时 -p 会失效。装好 CNI 才能像 docker 一样用。
坑二:在 K8s 节点上找不到容器。前面说过的 namespace 问题。K8s 的容器在 k8s.io 命名空间,记不住就 nerdctl -n k8s.io ps。
坑三:把 containerd 和 dockerd 一起跑。两者会各自管理一套镜像和容器,容易混淆,也浪费资源。如果决定迁移,就用一个不用另一个。真要并存,至少清楚你的容器在谁手里。
坑四:忘记 buildkit 就 nerdctl build。containerd 不内置构建功能,没有 buildkitd 就会报错。要么用上面的方式拉起 buildkit 容器,要么干脆在 CI 里构建好镜像再推送到 containerd。
坑五:容器不会自动重启。习惯 --restart always 的人迁移过来会踩坑。containerd 没有这个概念,必须用 nerdctl generate systemd 转成 systemd 服务,或者上层编排来管。
迁移实战:把一台跑的 Docker 站点换成 containerd
假设你有一台正在用 Docker 跑 Typecho 的 VPS,现在想把这套服务搬到 containerd 上,同时保留原有的数据卷。整个过程其实比你想象的简单,因为两者管理的都是标准的 OCI 镜像和标准目录。
第一步,先把原来 Docker 里的卷数据做个备份,这是任何迁移动作的前提:
docker stop web db
docker run --rm -v typecho_data:/data -v /root/backup:/backup \
alpine tar czf /backup/typecho_data.tar.gz -C /data .
# 数据库单独导出(用 mysqldump 最稳,比拷物理文件安全)
docker exec db mysqldump -uroot -p'密码' typecho > /root/backup/typecho.sql第二步,在 containerd 上用 nerdctl 拉取同样的镜像,并重新建卷。注意 Docker 时代的命名卷(named volume)在 nerdctl 里概念一致,但底层路径不同,所以更稳妥的做法是用宿主机目录挂载,把「数据放在哪」这件事显式写在命令里:
nerdctl pull typecho/typecho:latest
nerdctl volume create typecho_www
# 恢复备份到新卷
nerdctl run --rm -v typecho_www:/data -v /root/backup:/backup \
alpine sh -c "cd /data && tar xzf /backup/typecho_data.tar.gz"第三步,用 nerdctl 启动容器。语法和 docker run 一模一样:
nerdctl run -d --name web \
-p 80:80 \
-v typecho_www:/var/www/html \
--restart=no \
typecho/typecho:latest注意最后我特意写了 --restart=no——因为 containerd 体系里我们改用 systemd 来管自启,前面讲过用 nerdctl generate systemd 生成服务单元是这个场景的标准做法。这样即使 nerdctl 自身不提供守护,systemd 也会在崩溃后把容器拉起来。
第四步,验证。依次确认:容器在 nerdctl ps 里是 Up、站点首页能打开、数据(文章、评论、配置)都在、上传的图片能正常访问。四项全过,迁移就算完成。整套动作里最关键的不是命令,而是「先把数据备份出来,再谈迁移」这个纪律——有备份兜底,中间出任何问题都能回退。
和 Docker 的取舍:一张对照表
为了让你决策时有清晰的依据,把两者的关键差异列一下:
- 内存占用:Docker(dockerd + containerd)约 100–150MB 常驻;单独 containerd 约 40–60MB,低配机器差别明显。
- 命令体验:docker 命令更完善,构建、Compose、Swarm 一体;nerdctl 命令克隆度很高,但构建要额外装 buildkit,部分高级功能缺失。
- 镜像构建:Docker 内置;containerd 需 buildkit 配合。
- 自启动:Docker 有
--restart策略;containerd 需 systemd 或上层编排接管。 - 生态兼容:Docker 有庞大的一键部署脚本和文档;containerd 更适合 K8s 场景,纯手工部署的资料相对少。
- 网络:Docker 内置 bridge 与端口映射;containerd 依赖 CNI 插件。
把这张表对着自己的实际需求看一遍,答案基本就浮出来了。如果你追求的是「少折腾」和「生态丰富」,Docker 依然是首选;如果你在跑 K8s、或者特别在意资源占用、或者想贴近底层理解容器,那 containerd 会让你更舒坦。技术栈的选择不是赶时髦,而是匹配你每天真实要面对的工作。
个人站该不该从 Docker 迁到 containerd
结论是:大多数个人站没必要特意迁移。Docker 的便利性(特别是构建和 Compose)对个人站长来说价值很大,而 containerd 节省的那点内存,在一台 2G 内存的 VPS 上虽然有意义,但不足以抵消迁移成本。
但下面几种情况,值得认真考虑直接用 containerd:
- 你已经在跑单节点 Kubernetes(或 k3s),宿主机上本来就有 containerd,再装 dockerd 纯属多余,直接用 nerdctl 管理辅助容器最干净;
- 你的 VPS 内存特别紧张(比如 512M),Docker 的常驻开销占比太高,换成 containerd + nerdctl 能省出可观的内存;
- 你想深入理解容器技术本身,containerd 是比 Docker 更接近本质的一层,玩明白它对排障和理解 K8s 都有帮助;
- 你在做「站点容器化」但不想引入完整的 Docker 体系,只需要一个轻量的运行时。
技术选型没有绝对的对错。Docker 是「开箱即用的整套工具」,containerd 是「专注运行时的干净内核」。理解了两者的分层关系,你就能根据自己的场景做出判断,而不是盲目跟风「卸 Docker 换 containerd」或者死守 Docker 不放。对个人站长而言,最好的架构永远是那个你真正能维护、出问题能排查的架构。