资讯详情

资讯详情

containerd+nerdctl替代Docker的轻量方案:安装实战与避坑指南

1. 为什么要专门装一套nerdctlcontainerd从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引擎只需要containerdnerdctl就能用上几乎和docker一样的命令习惯。我大概是在Kubernetes节点上首次尝到这个甜头的。当时有几台节点要求尽可能精简组件不想再多跑一个Docker daemon但又需要能在节点上临时拉镜像、启容器做验证。直接拿ctr操作命令记不住且功能缺失装Docker又违背了精简组件的初衷。最后选定nerdctlcontainerd这套方案一下解决了所有顾虑。今天这篇文章就完整记录一下整个安装过程以及我在操作中踩过的坑和验证过的正确做法。内容适用场景想在Kubernetes节点上减少冗余组件、服务器本身资源有限不想跑完整Docker引擎、或者纯粹好奇containerd生态怎么用的读者。写作时我尽量做到照着操作就能复现不是那种跳步式教程。2. 安装前必须想清楚的三个选择版本搭配、系统环境、运行模式实际动手之前有三个方面建议先定下来。这三个选择会影响后面所有安装命令和配置文件没想清楚就直接抄网上的教程经常会出现命令一样但环境不同导致的各种奇怪报错。2.1 组件选型containerd、nerdctl、CNI插件要配套先说版本。containerd和nerdctl都有自己的版本线两个组件必须搭配着来不能随便各装各的最新版。我踩过一次坑containerd用1.5.xnerdctl直接上了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插件时它虽然能启动一个最简单容器但那种容器没有完整网络栈——相当于一个拓扑被孤立的小盒子只能自己跟自己玩。我最后确定的三件套版本组合组件版本说明containerd1.7.13当前1.7.x稳定线nerdctl1.7.0支持containerd 1.7CNI pluginsv1.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。生产集群节点其实大多是rootfulrootless主要用于多租户、安全要求极高的边缘节点。文章下面默认按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] Descriptioncontainerd container runtime Documentationhttps://containerd.io Afternetwork.target local-fs.target [Service] ExecStartPre-/sbin/modprobe overlay ExecStart/usr/local/bin/containerd Typenotify Delegateyes KillModeprocess Restartalways RestartSec5 LimitNPROCinfinity LimitCOREinfinity LimitNOFILEinfinity TasksMaxinfinity OOMScoreAdjust-999 [Install] WantedBymulti-user.target这个服务文件里值得注意的点Delegateyes让systemd彻底把cgroup控制权交给containerd后续容器能独立管理子cgroup这个设置对运行多容器非常重要。Restartalways保证意外退出后自动拉起生产节点都建议保留这个行为。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核心CLInerdctl同样是官方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也能跑但遇到异常容器清理和资源统计时容易碰上麻烦。我出于统一操作习惯一律设成SystemdCgrouptrue这个习惯帮助我省掉了后续混搭使用时的坑。修改配置之后需要重启containerdsystemctl 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 mirrorcontainerd 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节点下面具体写法放到下一小节。改完配置一定重启containerdsystemctl 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_NAMESPACEk8s.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 deniedsocket文件权限问题把非root用户加入containerd组或者用root身份执行。unable to find runtimerunc没有安装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。使用方式几乎照搬dockernerdctl 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则能进shUbuntu镜像则是bash。能不能exec成功和镜像自身是否带shell相关并不是配置错误。另外在Kubernetes节点上使用时Pod容器的日志默认被kubelet接管并写入journalnerdctl 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_PROXYhttp://your-proxy:port export HTTPS_PROXYhttp://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引擎的服务器nerdctlcontainerd确实是个很务实的组合它把容器运行时这层能力单独剥离出来装上就能用不需要背负Docker那套厚重的daemon和网络栈。如果你正打算精简容器组件或者正在维护Kubernetes节点建议直接从这套组合入手花上半天时间把所有命令验证一遍会发现日常使用体感非常顺滑。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →