1. 为什么要专门装一套nerdctl+containerd:从Docker逃逸说起
先做个坦白:我在很长一段时间里,都以为"容器就是想跑起来必须得靠Docker"。后来深入接触Kubernetes、看各种生产环境架构之后,才意识到一个事实——很多服务器上真正干活的容器运行时压根不是Docker daemon,而是containerd。Docker只是躲在后面套了一层壳,你用docker ps看到的那些容器,底层其实是containerd在管理。
那问题来了:既然containerd是那个"真正干活的",为什么大家还是习惯装Docker?
因为containerd本身的命令行工具太素了。它默认只带一个ctr命令,语法不友好,功能单薄,没有端口映射、没有卷挂载、没有build能力,连最常见的docker run -p用起来都费劲。你在生产环境排查问题时想用ctr看日志、查镜像,能明显感觉到它就是个"底层面向开发者的API工具",根本不适合日常运维。
nerdctl就是为了解决这个痛点出现的。它由containerd项目组维护,名字里的"nerd"就是从"container"里拆出来的,目标是做一个"containerd的Docker兼容CLI"。简单理解:你不需要装完整的Docker引擎,只需要containerd+nerdctl,就能用上几乎和docker一样的命令习惯。
我大概是在Kubernetes节点上首次尝到这个甜头的。当时有几台节点要求尽可能精简组件,不想再多跑一个Docker daemon,但又需要能在节点上临时拉镜像、启容器做验证。直接拿ctr操作,命令记不住且功能缺失;装Docker,又违背了精简组件的初衷。最后选定nerdctl+containerd这套方案,一下解决了所有顾虑。
今天这篇文章就完整记录一下整个安装过程,以及我在操作中踩过的坑和验证过的正确做法。内容适用场景:想在Kubernetes节点上减少冗余组件、服务器本身资源有限不想跑完整Docker引擎、或者纯粹好奇containerd生态怎么用的读者。写作时我尽量做到"照着操作就能复现",不是那种跳步式教程。
2. 安装前必须想清楚的三个选择:版本搭配、系统环境、运行模式
实际动手之前,有三个方面建议先定下来。这三个选择会影响后面所有安装命令和配置文件,没想清楚就直接抄网上的教程,经常会出现"命令一样但环境不同导致的各种奇怪报错"。
2.1 组件选型:containerd、nerdctl、CNI插件要配套
先说版本。containerd和nerdctl都有自己的版本线,两个组件必须搭配着来,不能随便各装各的最新版。我踩过一次坑:containerd用1.5.x,nerdctl直接上了0.20+,结果nerdctl要求的containerd API版本比实际版本高,容器启动时报"failed to invoke service"这一类接口兼容错误。
版本对应关系大致可以参考以下原则:
- containerd 1.6.x 搭配 nerdctl 1.x 系列没大问题。
- containerd 1.7.x 搭配 nerdctl 1.x 最新版本更稳妥。
- 如果你用的是containerd 1.5.x这种老版本,建议nerdctl选0.x老版本,比如0.17~0.18。
另外还有一个很容易被忽略的组件:CNI插件(v1.4.0以上版本)。nerdctl默认网络模式是不带端口映射的,相当于用了containerd内部的"insulated"网络。如果你想支持nginx这种需要外部访问的容器,特别是想用nerdctl run -p 8080:80进行端口映射,就必须安装CNI插件,并把插件二进制放到nerdctl会读取的目录下。
这里的实际逻辑是:nerdctl调用CNI插件创建容器网络命名空间和端口规则。没有CNI插件时,它虽然能启动一个最简单容器,但那种容器没有完整网络栈——相当于一个拓扑被孤立的小盒子,只能自己跟自己玩。
我最后确定的三件套版本组合:
| 组件 | 版本 | 说明 |
|---|---|---|
| containerd | 1.7.13 | 当前1.7.x稳定线 |
| nerdctl | 1.7.0 | 支持containerd 1.7 |
| CNI plugins | v1.4.0 | 提供bridge/portmap等必备插件 |
这个组合截至目前(写文章时)算是比较典型、验证充分的搭配,找问题也容易搜到解决方案。
2.2 系统环境检查:内核、防火墙和目录权限都不能含糊
三件套的版本定好之后,下一个问题是系统环境。
- 内核版本:containerd需要Linux内核5.x或以上的现代内核较好,3.10这种老内核在cgroup v2支持上会露怯。我一般在装之前统一执行uname -r看看。
- cgroup驱动:如果这台机器已经接入了Kubernetes集群,那么cgroup驱动必须和kubelet保持一致——通常选systemd,不能默认选cgroupfs。后面配置章节会具体说明。
- 防火墙/安全组:如果要做宿主机端口映射,确认ss或iptables规则的端口放行。这个不用多说,很多人卡在这一步去检查nerdctl命令却半天没结果。
还有目录权限问题。containerd默认的root目录是/var/lib/containerd,状态目录/run/containerd,日志输出到journal。如果这些目录被之前的Docker装过残留内容覆盖了,也会出乱子。干净的系统最好先删掉这些目录里的旧文件再来安装。
2.3 运行模式:rootful和rootless的选择
运行模式分成两种:
- rootful:以root用户启动containerd,容器进程默认也是root权限,安装简单,调试方便。
- rootless:使用普通用户启动containerd,配合rootlesskit,能实现无权限运行容器,安全性更好,但配置复杂度高了一截。
我的实际建议是:如果是学习、个人开发环境、或者单机验证场景,直接上rootful,别折腾rootless。生产集群节点其实大多是rootful,rootless主要用于多租户、安全要求极高的边缘节点。文章下面默认按rootful方式讲解,rootless想尝试的话我放一小节补充注意点。
3. 核心安装全程记录:containerd、nerdctl和CNI插件的落地操作
选择都敲定之后,我按以下步骤完整安装。每一步都附了命令,并会说明这步做完应该出现什么结果。
3.1 安装containerd:采用二进制包,不编译
用二进制包的意义在于:能拿到相对稳定的官方发布版本,不依赖发行版软件源里的旧版本——Ubuntu apt源里的containerd版本经常滞后,CentOS那边也差不多。我们实际运维过程中,组件小版本差距往往就是bug和安全漏洞的分界线。
# 1. 下载containerd 1.7.13的官方tar包 cd /tmp wget https://github.com/containerd/containerd/releases/download/v1.7.13/containerd-1.7.13-linux-amd64.tar.gz # 2. 解压并安装到/usr/local tar Cxzvf /usr/local containerd-1.7.13-linux-amd64.tar.gz上面这段命令值得具体拆一下:
- 用了
tar Cxzvf,其中C参数表示先切换目录再执行解压,C后面跟/usr/local。这么做可以把二进制直接释放到/usr/local/bin全路径,不用手动二次移动。 - 解压完可以用
which containerd检查,出现/usr/local/bin/containerd就是成功了。
接下来生成配置文件。containerd启动时默认会找/etc/containerd/config.toml,文件不存在的话会退回到内置默认配置,但为了后续做镜像加速和私有认证,最好先导出默认配置再改。
mkdir -p /etc/containerd containerd config default > /etc/containerd/config.toml此时可以先用默认配置启动一次验证它能不能跑起来:
systemctl enable containerd --now # 或者不用systemd的话 # containerd &因为我习惯用systemd管理长期服务,所以把containerd.service服务单元文件补上。很多人的containerd是Kubernetes节点上自带的那种systemd服务,可以直接复用;如果是裸机,需要手写一个:
[Unit] Description=containerd container runtime Documentation=https://containerd.io After=network.target local-fs.target [Service] ExecStartPre=-/sbin/modprobe overlay ExecStart=/usr/local/bin/containerd Type=notify Delegate=yes KillMode=process Restart=always RestartSec=5 LimitNPROC=infinity LimitCORE=infinity LimitNOFILE=infinity TasksMax=infinity OOMScoreAdjust=-999 [Install] WantedBy=multi-user.target这个服务文件里值得注意的点:
Delegate=yes让systemd彻底把cgroup控制权交给containerd,后续容器能独立管理子cgroup,这个设置对运行多容器非常重要。Restart=always保证意外退出后自动拉起,生产节点都建议保留这个行为。ExecStartPre提前加载overlay模块,属于安全冗余,一般2.6.37之后的内核都自带overlay模块,不会报错;但如果你的内核因为某种原因没启用,这行会直接提示。
写完之后daemon-reload并启动:
systemctl daemon-reload systemctl start containerd systemctl status containerd # 看到 active (running) 就说明daemon起来了此时验证一下底层组件:ctr version应该能打印出containerd的版本号。这步做完,containerd本体安装成功。
3.2 安装nerdctl:核心CLI
nerdctl同样是官方release二进制包,下载解压后放进/usr/local/bin即可。
cd /tmp wget https://github.com/containerd/nerdctl/releases/download/v1.7.0/nerdctl-1.7.0-linux-amd64.tar.gz tar Cxzvvf /usr/local nerdctl-1.7.0-linux-amd64.tar.gz注意这儿有一个分版本的区别:新版本nerdctl的tar包里可能还包含containerd-rootless-setconfig等额外工具,解压后你可以看到bin列表。直接用nerdctl version验证,如果能显示出Client和Server版本信息,说明它已经成功连上了containerd的GRPC接口。
如果你运行nerdctl version卡住或者报"cannot connect to containerd",先启动containerd服务,再确认/var/run/containerd/containerd.sock这个socket文件存在。nerdctl默认通过这个Unix socket和containerd通信。
3.3 安装CNI插件:跑通端口映射的前提
CNI插件是那个决定网络功能的分水岭。你在网上看到有人说"nerdctl跑不了-p端口映射",十有八九就是漏了这一步。
cd /tmp wget https://github.com/containernetworking/plugins/releases/download/v1.4.0/cni-plugins-linux-amd64-v1.4.0.tgz mkdir -p /opt/cni/bin tar Cxzvf /opt/cni/bin cni-plugins-linux-amd64-v1.4.0.tgz这一步执行完后/opt/cni/bin下面应该有bridge、portmap、firewall、host-local这些网络插件文件。它们的职责分工是这样的:
- bridge:创建容器使用的虚拟网桥。
- host-local:分配IP地址。
- portmap:处理宿主机端口到容器端口的映射,就是-p参数背后的实现。
- firewall:给容器流量做iptables规则隔离。
另外还要准备nerdctl能识别的CNI网络配置目录/etc/cni/net.d。其实不创建这个目录,nerdctl首次运行带网络模式的容器时会自动生成自己的默认网络配置,但提前创建目录总归更稳妥,避免文件权限问题导致的隐藏报错。
mkdir -p /etc/cni/net.d到此,三个核心组件都装完了。下一步是修改containerd配置,其中包含一个关键的cgroup驱动设置。
3.4 配置cgroup驱动:systemd还是cgroupfs
编辑/etc/containerd/config.toml,找到[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]段。如果这行段名不存在,可以手动补充。
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] SystemdCgroup = true为什么一定要把SystemdCgroup设为true?
这要回到Linux cgroup的管理方式。cgroup v2统一了进程资源控制的层级,内核预留了两条管理路径:一条是systemd在开机时生成的cgroup树(systemd模式),另一条是容器运行时手工创建的独立层级(cgroupfs模式)。如果容器运行时用cgroupfs、Kubernetes用systemd,两套树互相抢占资源,节点会反复报cgroup-driver mismatch或内存泄漏类故障。Kubernetes官方要求的做法是让所有组件统一到systemd驱动下。
如果只是普通服务器跑nerdctl,不接入K8s,用cgroupfs也能跑,但遇到异常容器清理和资源统计时容易碰上麻烦。我出于统一操作习惯,一律设成SystemdCgroup=true,这个习惯帮助我省掉了后续混搭使用时的坑。
修改配置之后需要重启containerd:
systemctl restart containerd重启之后再跑一次ctr version确认没有连接问题。
3.5 顺带说说rootless(普通用户跑nerdctl)的情况
如果你的需求确实是非root用户跑nerdctl,核心过程是这样的:
- 安装rootlesskit和slirp4netns(提供用户态网络)。
- 用rootless用户执行
containerd-rootless-setuptool.sh install,自动配置用户级systemd服务。 - 用同一用户执行
nerdctl info验证连通。
rootless模式对CNI也有额外要求,必须使用fuse-overlayfs或者在配置里切换到vfs快照器。这里面的体验差异很明显:容器的overlayfs在rootless下默认安全性受限,读写性能也会比rootful模式低一截。所以我明确推荐:没有强隔离需求就选rootful,省心。
4. 配置细节:镜像下载加速和私有仓库认证
组件装完不代表环境就通了。你很快会遇到第一个实际障碍:从Docker Hub拉镜像极慢。国内的网络环境,不配置镜像加速,随手拉一个nginx镜像都可能卡到天荒地老。
4.1 在config.toml里配置registry mirror
containerd 1.7.x的配置文件结构比较深,修改点位于[plugins."io.containerd.grpc.v1.cri".registry]段下。目标是把仓库地址映射到一个国内可访问的镜像站。
打开/etc/containerd/config.toml,在[plugins."io.containerd.grpc.v1.cri".registry]下添加如下配置:
[plugins."io.containerd.grpc.v1.cri".registry.mirrors] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] endpoint = ["https://docker.m.daocloud.io", "https://dockerproxy.com"]这段配置的含义:containerd在拉取docker.io上的镜像时,不是直接访问Docker Hub,而是先尝试https://docker.m.daocloud.io这个加速地址,失败再尝试后面的备用地址。
这里有个细节要提醒:老版本配置文件里还会出现[plugins."io.containerd.grpc.v1.cri".registry.configs]这种结构,用于配置私有仓库的tls和认证信息。但新版本中认证字段的写法略有变化,很多人照着旧文档复制粘贴后,重启containerd必然报错。正确的私有认证段落其实不在configs里,而是挂在registry.configs节点下面,具体写法放到下一小节。
改完配置一定重启containerd:
systemctl restart containerd然后立刻测试:
nerdctl pull nginx:alpine如果这步拉不下来,排查方向主要有两个:要么endpoint地址本身不可达(可以curl验证),要么配置段位置放错了导致containerd没读到镜像加速配置。这算是我在配置中最容易卡住的地方。
4.2 私有Harbor仓库的认证配置
生产环境通常有自己的私有镜像仓库,最常见的是Harbor。containerd对Harbor的认证配置藏得比较深。假设你的Harbor地址是registry.internal.example.com,配置如下:
[plugins."io.containerd.grpc.v1.cri".registry.configs."registry.internal.example.com".auth] auth = "dXNlcjpwYXNzd29yZA=="这里auth字段的值是"用户名:密码"做Base64编码的结果。可以用一行命令生成:
echo -n "admin:YourPassword123" | base64注意细节:
- 如果Harbor启用了自签名证书,还得在
tls子段里配置ca_file,或者直接设置insecure_skip_verify = true(仅限内网环境)。 - 配置之后,重启containerd并执行
nerdctl pull registry.internal.example.com/your-project/app-image:v1.0验证。 - 如果报401或者
unauthorized,优先检查Base64字符串里有没有意外换行,echo -n参数丢了就很难发现。
这套认证写法同样适用于其他带鉴权的私有仓库。配好之后,生产环境拉镜像不愁认证问题。
4.3 命名空间:区分系统容器和业务容器
使用containerd时你会注意到一个陌生概念——namespace。这是containerd管理容器的一个逻辑分组机制,类似于Kubernetes里的namespace,用来隔离不同的业务域。
比如你在Kubernetes节点上跑nerdctl,会发现nerdctl ps默认看不到kube-system这种命名空间里的Pod容器。需要加--namespace k8s.io参数才能看到:
nerdctl --namespace k8s.io ps -a这个区别非常重要。很多人装完nerdctl后兴致勃勃跑nerdctl ps,发现系统一直为空,就怀疑containerd坏了。其实不是坏了,是kubelet创建的Pod容器放在了k8s.io命名空间里,和nerdctl默认的default命名空间不互通。
如果你希望nerdctl的默认命名空间就是k8s.io,可以设置环境变量:
export NERDCTL_NAMESPACE=k8s.io不过我更推荐的思路是:独立运维机器上用default命名空间保持干净,接入集群的节点按需加--namespace参数操作。时刻意识到"namespace隔离"的存在,比盲目把所有东西塞进一个空间更安全。
5. 实际功能验证:hello world、端口映射和镜像构建
配置都处理完了,现在进入验证阶段。验证不是简单跑一个容器就算通过,而是要把日常会用到的高频能力逐项过一遍,确认没遗漏。
5.1 先跑个hello world测试最小链路
nerdctl run --rm hello-world如果能打印出"Hello from Docker!"那段经典文案,意味着你的containerd、nerdctl、镜像拉取链路完全跑通。如果报错,我列几个高频问题的排查经验:
failed to resolve reference:镜像加速没生效或者网络不通,先检查config.toml里endpoint配置。permission denied:socket文件权限问题,把非root用户加入containerd组或者用root身份执行。unable to find runtime:runc没有安装,nerdctl虽然装了但还需要runc配合,使用apt安装runc即可。
顺便说一下,containerd本身自带了一个snapshotter概念,默认用overlayfs,镜像层存储结构直接复用这个设计。hello-world这种超小镜像跑通,对overlayfs的加载逻辑也是一种验证。
5.2 端口映射和卷挂载:最容易踩雷的部分
接着验证nginx服务型容器:
nerdctl run -d --name web -p 8080:80 nginx:alpine curl http://localhost:8080如果curl能返回nginx欢迎页,说明CNI插件工作正常。我在这儿标记几个异常现象和原因:
异常现象1:容器虽然创建,但无法访问端口。
原因大概率是/opt/cni/bin没配好,nerdctl创建网络时报"failed to setup network"。解决方案就是把CNI插件补装到位,再重启containerd。
异常现象2:第一次启动带-p参数的容器时特别慢。
这通常是CNI插件首次加载bridge和portmap模块,需要创建虚拟网卡、添加iptables规则。正常的初始化时间大概几秒。如果超过30秒还不成功,去查/var/log/containerd日志或者journal里有没有权限相关报错,常见是iptables: Permission denied。该场景下需要确认用户本身的网络权限,rootful模式下没这个问题。
卷挂载这部分没有特别坑的地方,语法和docker保持一致:
nerdctl run -d -v /data/www:/usr/share/nginx/html:ro -p 8080:80 nginx:alpine如果容器内需要写/data/www,记得把:ro去掉。权限问题大多出在宿主机目录SELinux上下文上,实在不行临时chcon -t container_file_t /data/www。
5.3 nerdctl build:镜像构建能力验证
containerd默认没有build能力,nerdctl把它补上了,底层依赖BuildKit。使用方式几乎照搬docker:
nerdctl build -t myapp:v1.0 .这条命令会读取当前目录的Dockerfile并执行构建。但任一步骤都可能踩到版本兼容问题,最常见的报错是failed to solve: frontend dockerfile.v0: unexpected status from POST request to /...,这通常是BuildKit容器内部拉取基础镜像失败,和外部网络或仓库认证有关。
排障建议:把构建过程加上--debug参数,能看到更多日志:
nerdctl build --debug -t myapp:v1.0 .构建完成之后,可以顺带把镜像推送到私有Harbor测试凭证是否生效:
nerdctl tag myapp:v1.0 registry.internal.example.com/myapp:v1.0 nerdctl push registry.internal.example.com/myapp:v1.0如果push阶段报了unauthorized,回到上一步认证配置检查Base64内容。我在实际业务中经常推镜像到Harbor,整个链路开后,后续CI/CD发版就完全不需要Docker引擎了。
5.4 日志和进入容器操作
日常运维逃不开的还有日志查看和容器exec命令。nerdctl的语法同样照着docker来:
nerdctl logs -f web nerdctl exec -it web sh注意一点:nginx官方镜像没有内置shell,如果镜像基于alpine则能进sh,Ubuntu镜像则是bash。能不能exec成功和镜像自身是否带shell相关,并不是配置错误。
另外,在Kubernetes节点上使用时,Pod容器的日志默认被kubelet接管并写入journal,nerdctl logs直接查不到。这种情况下不如直接用kubectl logs -n namespace pod-name,更符合集群运维习惯。
6. 实测排坑记录:安装和日常使用中容易踩的雷
写到最后,我把真实的踩坑经历整理成这份问题清单。每一条都是我或身边同事实际撞过墙、最后通过日志和源码定位才解决的。
6.1 "no such child process"类错误:runc兼容问题
有一次装完containerd和nerdctl后,跑nerdctl run直接报failed to start container: failed to start shim: no such child process。
观察这个问题时,我先查了runc版本,发现runc是发行版自带的很老版本(1.0.x可能都没到),和containerd 1.7默认的shim v2版本不匹配。解决方案是重装runc到官方release版本,下载https://github.com/opencontainers/runc/releases/download/v1.1.9/runc.amd64放到/usr/local/bin并赋予可执行权限。
这个坑特别典型,因为runc往往被忽略。容器运行的完整链路是nerdctl -> containerd -> containerd-shim -> runc,每一环缺一不可。装完nerdctl只验证到了containerd是通的,runc版本过旧,到启动那一步才会暴露。
6.2 exec和build命令报错:BuildKit容器内部网络异常
在海外服务器上构建镜像时一切正常,但到了国内网络环境下,nerdctl build经常在拉基础层时卡住。原因是BuildKit容器本身没有走nerdctl配置的镜像加速通道,仍然直接访问Docker Hub。
排查办法是把BuildKit容器也归入镜像加速通道的管辖范围。有些版本需要做额外设置,但更简单的办法是手动设置BuildKit的代理环境变量:
export HTTP_PROXY=http://your-proxy:port export HTTPS_PROXY=http://your-proxy:port或者直接换一个延迟更低的基础镜像源。这类问题在不同网络环境下差异很大,没有通用解。我的建议是:先测curl https://registry-1.docker.io/v2/通不通,不通就统一配置代理。
6.3 containerd.sock权限问题:非root用户管不了容器
安装完成后来回切用户执行命令时,非root用户经常看到permission denied while trying to connect to the /run/containerd/containerd.sock。那是因为socket文件的默认权限只让root用户访问。
两个解法任选:
- 把用户加入
containerd组并重新登录:
groupadd containerd usermod -aG containerd your-user chown :containerd /run/containerd/containerd.sock- 或者改socket文件权限为0660并重启containerd。
这个坑虽然小,但很折磨人。尤其自动化脚本里用了非root执行nerdctl命令时,排查一圈最后发现是权限,真的很崩溃。
6.4 同一台机器上Docker和containerd共存:镜像缓存互不共享
很多人的服务器上原本装了Docker,现在又要加装nerdctl。Docker和containerd同时运行时,两者各自管理自己的镜像仓库,不会自动共享缓存。
这意味着:你在Docker里已经拉好的nginx镜像,用nerdctl还要再拉一遍。存储占用翻倍是正常现象。想彻底迁移的话,可以用nerdctl pull --all-platforms批量拉取,或者把Docker镜像docker save后导出,再nerdctl load导入。
我遇到过一种更微妙的场景:Docker daemon自带的containerd版本被覆盖。因为你手动装了新版containerd到/usr/local/bin,而Docker默认可能引用/usr/bin里的旧版本,导致systemd服务启动时出现"binary mismatch"。解决办法是把Docker的containerd二进制路径明确指定,或者干脆卸载Docker,只留一套containerd。生产环境除非确实需要,一般不建议两个并行。
6.5 关于命令习惯的迁移效率
最后聊个经验性的内容。从docker切到nerdctl,命令基本不用重新学,绝大多数情况就是把docker改成nerdctl。但有三点必须额外注意,是实际上手后容易搞混的地方:
- containerd的namespace隔离机制,上文已详细说过。
- 默认网络模式不支持跨主机通信,不像Docker的swarm或overlay网络。跨节点通信建议交给Kubernetes或Flannel/Cilium这类工具去管。
- 部分docker命令参数nerdctl还没完全实现,例如
docker update这种动态调整资源限制的命令,nerdctl当前版本还不支持。提前查文档确认要用的关键参数在不在支持列表里,别等到脚本跑一半才发现能力缺失。
基于以上体验,我自己的态度是:对于不需要完整Docker引擎的服务器,nerdctl+containerd确实是个很务实的组合;它把"容器运行时"这层能力单独剥离出来,装上就能用,不需要背负Docker那套厚重的daemon和网络栈。如果你正打算精简容器组件,或者正在维护Kubernetes节点,建议直接从这套组合入手,花上半天时间把所有命令验证一遍,会发现日常使用体感非常顺滑。