开头先聊个实际场景:生产环境的 Kubernetes 节点上,镜像拉不下来、私有仓库连不上、证书报错、加速器失效,十次有八次问题都出在 containerd 的镜像仓库配置上。Docker 时代改 /etc/docker/daemon.json 就能解决的事,换成 containerd 之后很多人一下子抓瞎——配置路径变了、写法变了、生效方式也变了。这篇文章就从一张配置文件讲起,把 containerd 配镜像仓库这件事彻底掰开揉碎:从私有仓库、HTTP 仓库到 mirror 加速,再到 Kubernetes 调用 containerd 的完整链路,最后附上我这些年踩过的坑和排查思路。无论你是在裸机环境用 nerdctl,还是在生产集群里管节点,这篇文章都值得照着走一遍。
1. 为什么 containerd 的仓库配置比 Docker 麻烦
1.1 从 Linux 内核接口到运行时,containerd 到底管什么
containerd 是从 Docker 里拆分出来的容器运行时,负责镜像管理、容器生命周期、网络挂载这些底层能力。它提供的是一个 gRPC 服务,默认监听在 /run/containerd/containerd.sock。上层无论是 Docker、Kubernetes 还是 nerdctl,最终都是通过这个 socket 跟 containerd 打交道。
和 Docker 的客户端-守护进程模式不同,containerd 没有 docker pull 这种高层命令,它暴露的是一组更底层的 API。所以配置镜像仓库时,不能再用“改 daemon.json 然后重启 docker”这种惯性思维,而是直接改 containerd 自身的 TOML 配置。
另外要明确一点:Kubernetes 在 1.24 版本之后就彻底移除了 dockershim,所有节点都通过 CRI(Container Runtime Interface)直接对接 containerd。这意味着你现在配的 containerd 镜像仓库规则,会直接决定整个集群能不能正常拉取业务镜像。这不是“某个工具的小配置”,而是集群稳定性的命门。
1.2 镜像仓库配置的本质作用
镜像仓库配置本质上是回答 containerd 三个问题。
第一个问题:访问某个仓库时走哪个地址。containerd 默认把 docker.io 当作官方仓库,拉 nginx:latest 实际上请求的是 registry-1.docker.io/library/nginx:latest。如果你内网有自建仓库,或者要用某个加速器,就得告诉 containerd 换地址。
第二个问题:访问时需要带什么凭证。私有仓库一般都有认证,containerd 需要知道用户名密码、token、客户端证书这些信息。
第三个问题:访问时信任哪些证书。自建仓库如果用自签名证书,containerd 默认不信任,必须把证书加进去,或者明确告诉它跳过验证。
1.3 什么时候必须动这个配置
我把实际工作中需要改 containerd 仓库配置的场景列一下:
- 内网部署了一个 Harbor,镜像都要从 Harbor 拉取;
- 云服务器上拉 Docker Hub 官方镜像超时,需要配置内部加速源;
- 构建机或测试环境里有一个只走 HTTP 的本地仓库,端口一般是 5000;
- 生产环境用了自签名证书的私有仓库,每次拉镜像都报
x509: certificate signed by unknown authority; - Kubernetes 集群里通过
imagePullSecrets拉私有镜像,但节点上一直 401; - 公共镜像源在国外,某些环境访问极慢,需要配置可用的 mirror。
每一条背后都是实际故障,每一条最终都归结到同一个动作:写对 containerd 的镜像仓库配置。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手前必须搞懂的三类仓库配置
2.1 带 HTTPS 证书认证的私有仓库
这种仓库最典型的就是 Harbor,尤其很多企业会在内网或者私有云环境用自签名证书部署。containerd 访问这类仓库时,需要完成两层信任:
第一层是“信任这个服务器”,也就是校验服务端证书的有效性。如果证书是自签的,或者不是知名 CA 签发的,containerd 默认会拒绝连接。这时你需要把 CA 证书提供给 containerd,让它认这个证书。
第二层是“验证我是谁”,也就是仓库的账号认证。Harbor 这类仓库通常要求登录,containerd 需要在请求头里带上 Basic Auth 的凭证。
这两层信任必须同时成立,否则都会报 401 或者 x509 错误。很多运维只配了账号密码,忘了加 CA,结果卡在证书上,这种案例我见过太多次。
2.2 走 HTTP 的内网仓库
测试环境、开发环境、边缘机房里,经常有一个裸奔的 HTTP 仓库,地址形如 http://192.168.1.100:5000。containerd 默认是不允许 HTTP 明文访问的,它会用 HTTPS 去连,然后得到 http: server gave HTTP response to HTTPS client 的报错。
处理方式有两种:一是把仓库升级成 HTTPS;二是明确告诉 containerd“这个地址允许走 HTTP”。内网环境通常选第二种,代价是流量不加密,但这在隔离网络里是完全可以接受的。
注意:走 HTTP 的仓库地址一定要写完整协议,写成 http://192.168.1.100:5000,千万别写成 https://192.168.1.100:5000,否则会一直卡在 TLS 握手失败上。
2.3 镜像加速源 mirror
mirror 翻译过来就是镜像加速器,它解决的是“仓库源本身访问慢”的问题。大厂的公有云、学校或机构内部通常都维护着 Docker Hub 的缓存同步节点。你要做的事,是把 containerd 拉 docker.io 镜像时的请求,改发到这些加速地址上。
mirror 和私有仓库的配置位置很接近,但语义不同:mirror 是“去那个仓库也拉镜像,但目标仓库是原仓库的镜像源”,而 HTTPS 私有仓库是“直接把某个地址当作真正的仓库”。containerd 支持给一个仓库配置多个 mirror endpoint,拉不到就换下一个,这个机制在生产环境很实用。
3. 核心实操:修改 config.toml 的完整过程
3.1 先找到你的配置文件
containerd 的配置文件默认在 /etc/containerd/config.toml。大多数发行版安装后的默认配置是空的或者非常精简,可以用下面的命令生成一份完整的默认配置:
bash复制mkdir -p /etc/containerd
containerd config default > /etc/containerd/config.toml
执行完记得先备份一份:
bash复制cp /etc/containerd/config.toml /etc/containerd/config.toml.bak
这里要留意版本差异。containerd 1.x 和 2.x 的配置结构有变化:1.x 时代,镜像仓库相关配置写在 [plugins."io.containerd.grpc.v1.cri".registry] 下面;2.x 把 CRI 配置从主配置里拆出去了,更推荐用独立的 hosts.toml 文件来管理仓库配置。下面的步骤我两种都覆盖。
3.2 配置 HTTPS 私有仓库(Harbor 场景)
如果你用的是 containerd 2.x 或者最新版 1.x,最推荐的方式是先在主配置文件里开启 config_path:
toml复制version = 2
[plugins."io.containerd.grpc.v1.cri".registry]
config_path = "/etc/containerd/certs.d"
然后为每个仓库单独创建目录和文件。以 registry.example.com 为例,需要创建:
bash复制mkdir -p /etc/containerd/certs.d/registry.example.com
在 /etc/containerd/certs.d/registry.example.com/hosts.toml 里写入:
toml复制server = "https://registry.example.com"
[host."https://registry.example.com"]
ca = "/etc/containerd/registry-ca.crt"
[host."https://registry.example.com".header]
authorization = "Basic dXNlcjpwYXNzd29yZA=="
其中 ca 指向 Harbor 的 CA 证书文件,authorization 的值是 用户名:密码 做 Base64 编码后的结果。生成方式:
bash复制echo -n "admin:你的密码" | base64
然后重启 containerd:
bash复制systemctl restart containerd
测试拉取:
bash复制crictl pull registry.example.com/myproject/myapp:v1.0
如果 crictl 还没配置,先写 /etc/crictl.yaml:
yaml复制runtime-endpoint: unix:///run/containerd/containerd.sock
image-endpoint: unix:///run/containerd/containerd.sock
timeout: 10
debug: false
3.3 配置 HTTP 免认证仓库(内网场景)
还是用 config_path 的方式,假设内网仓库地址是 http://192.168.1.100:5000:
bash复制mkdir -p /etc/containerd/certs.d/192.168.1.100:5000
对应的 hosts.toml 内容:
toml复制server = "http://192.168.1.100:5000"
[host."http://192.168.1.100:5000"]
capabilities = ["pull", "resolve", "push"]
注意两点:第一,server 和 host 的地址必须带 http:// 前缀;第二,capabilities 声明了这个仓库允许哪些操作,默认就是 ["pull", "resolve"],如果你还要往这个仓库推镜像,必须显式加上 "push"。
如果你用的是 containerd 1.x 旧格式,则在 config.toml 里写:
toml复制[plugins."io.containerd.grpc.v1.cri".registry.configs]
[plugins."io.containerd.grpc.v1.cri".registry.configs."192.168.1.100:5000".tls]
insecure_skip_verify = true
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."192.168.1.100:5000"]
endpoint = ["http://192.168.1.100:5000"]
insecure_skip_verify 这个参数的意思是“跳过证书验证”,用于自签名证书场景;配合 http:// endpoint 使用,就能访问 HTTP 明文仓库。
3.4 配置加速源(mirror 场景)
加速源配置同样放在 hosts.toml。以配置 docker.io 的加速为例:
bash复制mkdir -p /etc/containerd/certs.d/docker.io
/etc/containerd/certs.d/docker.io/hosts.toml:
toml复制server = "https://registry-1.docker.io"
[host."https://docker.mirrors.example.com"]
capabilities = ["pull", "resolve"]
这里 server 还是默认的 Docker Hub 官方地址,而 host 里写的是加速器地址。containerd 拉 docker.io 的镜像时,会先尝试访问 host 列表里的地址,失败后再回退到 server。
如果你用 containerd 1.x 旧格式,加速配置长这样:
toml复制[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"]
endpoint = [
"https://加速器地址1",
"https://加速器地址2",
]
3.5 验证配置是否生效
配置改完不能只看“重启没报错”就完事,必须实打实拉一次镜像验证。我的标准流程是:
第一步,检查 containerd 配置是否合法:
bash复制containerd config dump > /dev/null && echo "config ok"
有报错就按提示改。
第二步,重启:
bash复制systemctl restart containerd
systemctl status containerd --no-pager -l
第三步,用 crictl 拉镜像:
bash复制crictl pull nginx:1.27
第四步,查看本地镜像:
bash复制crictl images
如果你机器上装了 nerdctl,还可以这样验证:
bash复制nerdctl --namespace k8s.io images
注意命名空间要指定为 k8s.io,因为 Kubernetes 创建的容器运行在 k8s.io 命名空间下,用 ctr 或 nerdctl 不指定命名空间是看不到的。
提示:
ctr、crictl、nerdctl三个命令都能操作 containerd,但侧重点不同。crictl是标准 CRI 工具,走的是 Kubernetes 的 CRI 接口;ctr是 containerd 原生命令;nerdctl是 Docker CLI 风格的 containerd 客户端。日常排查节点镜像问题,优先用crictl。
4. Kubernetes 是如何调用 containerd 的
4.1 从 kubelet 到 CRI 再到 containerd
很多运维接触 containerd 都是在 Kubernetes 节点上,平时只会 systemctl restart containerd,遇到镜像问题就抓瞎。但如果你理解了 kubelet 和 containerd 之间的调用关系,再回去看仓库配置就会豁然开朗。
Kubernetes 的 kubelet 进程并不直接操作容器,它通过 CRI(Container Runtime Interface)这个统一接口去调用运行时。CRI 是 gRPC API,定义了一堆 PullImage、RunPodSandbox、CreateContainer、StartContainer 这样的远程方法。containerd 内部内置了一个 cri plugin,负责实现这些方法,并监听在 /run/containerd/containerd.sock 上。
所以一条完整调用链是这样:
code复制kubelet → CRI(gRPC) → /run/containerd/containerd.sock → containerd cri plugin → containerd 核心 → runc/其他运行时
kubelet 并不知道底层是 containerd 还是其他运行时,它只认 CRI 接口。containerd 则负责把 CRI 请求转成对 runc 等低层运行时的操作。
4.2 镜像拉取在哪个环节发生
你可以把 Pod 的创建过程想象成一次完整的“点菜”流程:
kubelet 从 apiserver 同步到 Pod 定义后,第一步是调用 RunPodSandbox,containerd 会为这个 Pod 创建一个 sandbox 容器。这个 sandbox 容器使用的镜像是 pause,作用是给同一个 Pod 里的业务容器准备网络、PID 等命名空间。
第二步才是拉取业务镜像,kubelet 调用 CRI 的 PullImage 方法,把镜像准备到节点本地。如果节点上已经有这个镜像,并且 tag 匹配,这一步可以跳过。
第三步是 CreateContainer,containerd 根据镜像创建容器,但还没启动。
第四步是 StartContainer,真正启动容器进程。
镜像仓库配置只影响第二步 PullImage。仓库地址、证书、凭证、mirror 这些规则,最终都是出现在 containerd 的 registry 配置里。
4.3 一个拉取失败的实战排查路径
我举个实际例子:集群里有一个 Deployment 用私有仓库镜像,一直 ImagePullBackOff。
第一步看事件:
bash复制kubectl describe pod xxx-xxx
如果事件里写 Failed to pull image "registry.example.com/app:v1": rpc error: code = Unknown desc = failed to pull and unpack image ... x509: certificate signed by unknown authority,说明是证书问题。
此时按前面 3.2 的步骤,给节点上的 containerd 配置 CA 证书,或者临时用 skip_verify 测试。
如果事件写 unauthorized 或 401,说明是凭据问题。此时检查两个地方:一是节点上 containerd 是否配了正确的 Basic Auth;二是 Pod 是否引用了 imagePullSecrets。
在 Kubernetes 里创建 Secret 并让 Pod 引用:
bash复制kubectl create secret docker-registry regcred \
--namespace=default \
--docker-server=registry.example.com \
--docker-username=admin \
--docker-password=xxx \
--docker-email=xxx@example.com
然后编辑 Deployment:
yaml复制spec:
template:
spec:
imagePullSecrets:
- name: regcred
要理解的是,imagePullSecrets 的信息最终也会通过 CRI 传给 containerd,由 containerd 在拉取镜像时自动拼到请求头里。所以在节点上配置和用 Secret 配置,底层逻辑是一样的,只是入口不同。
5. 常见问题排查与避坑实录
5.1 x509 证书报错的排查思路
x509: certificate signed by unknown authority 是私有仓库配置里最常见的报错。出现这个报错的核心原因只有一个:containerd 不认仓库的证书。
解决办法按顺序排查:
- 确认证书文件存在且是 PEM 格式,
openssl x509 -in 你的证书 -noout -text能看到内容才算有效; - 确认
hosts.toml里的ca路径写的是绝对路径,containerd 进程用户(通常是 root)有权限读取; - 确认
server字段的域名和证书里的域名匹配,证书是给registry.example.com签的,你配置里就不能写https://192.168.1.100; - 确认 containerd 真的读到了你的配置。用
containerd config dump看最终生效配置,别只看文件写没写。
注意:不要因为嫌麻烦就直接
insecure_skip_verify = true。跳过证书验证意味着中间人可以完全冒充你的镜像仓库,生产环境千万慎用。临时调试可以,事后必须补上正式证书。
5.2 HTTP 仓库连不上的处理
报错 http: server gave HTTP response to HTTPS client 是最典型的 HTTP/HTTPS 协议不匹配问题。containerd 默认走 HTTPS 去访问仓库,你的仓库只提供 HTTP,两边就掐起来了。
解决方法是把 endpoint 写成完整的 http:// 协议头,并且如果遇到证书问题还要配合 insecure_skip_verify = true。这里有个细节:hosts.toml 里的 host 键名也要带 http://,否则 containerd 可能不识别。
5.3 配置改了不生效
这个问题也很常见。改完 /etc/containerd/config.toml 后没有重启 containerd,或者重启了但 systemd 显示的是旧的进程。
另外,如果你用了 config_path 这种外部配置目录,改了 hosts.toml 之后,有些老版本 containerd 并不会热加载,同样需要重启。有些新版本支持 SIGHUP 信号做平滑重载,但为了稳定性,我建议一律 systemctl restart containerd。
还有一个坑:config_path 和旧的内联配置同时存在时,以 config_path 为准,内联部分可能被忽略。如果你在 config.toml 里写了旧的 mirror 配置,又保留了 config_path,后面你会发现旧配置怎么改都不生效,因为 containerd 根本不读它。
5.4 docker.io 拉取超时或限流
Docker Hub 对匿名拉取有速率限制,加上某些环境下访问 Docker Hub 本身就不稳定,就会表现为拉镜像超时、Too Many Requests、或者一直 retrying。
对策有两个方向:一是配置一个稳定的 mirror 加速源;二是如果公司内网有 Harbor,并且 Harbor 配置了 Docker Hub 代理缓存,直接把 docker.io 的 host 指向内网 Harbor 地址。
配置示例:
toml复制server = "https://registry-1.docker.io"
[host."https://harbor.example.com/dockerhub"]
capabilities = ["pull", "resolve"]
这里 harbor.example.com/dockerhub 是你的 Harbor 里创建的“Docker Hub 代理项目”。这样拉 docker.io 的镜像时,containerd 会优先走内网,速度和稳定性都会好很多。
5.5 私有仓库的镜像 tag 写全
这个坑是新人最容易忽略的:私有仓库镜像写成 myapp:1.0,以为是完整 tag,但 containerd 会把它解析成 docker.io/library/myapp:1.0,当然拉不到。
私有仓库的镜像必须写全仓库地址,比如 registry.example.com/myproject/myapp:1.0。如果是 Harbor,镜像名长这样:192.168.1.100/project-name/image-name:tag。少了任何一个部分,解析出来的地址就完全不对。
5.6 hosts.toml 版本的几个关键细节
最后再强调几个 hosts.toml 的细节:
server字段不写会报server is not set错误,但是可以写成空串来显式表示“只从 host 拉取”,不过新手不要这么做;capabilities支持pull、resolve、push三个值,默认是pull和resolve;skip_verify只跳过宿主机的证书验证,不等同于“免认证”,仓库要求登录时照样要配header;- 多个
host之间是顺序尝试的关系,第一个拉取失败会尝试下一个,所以把最稳的加速器放最前面; - 配置修改后,用
crictl info可以查看 containerd 的运行时和镜像信息,这个命令比直接看配置更有用,因为它展示的是“运行时真正生效的配置”。
6. 实操总结与个人心得
我在实际运维中折腾过太多回镜像仓库配置,从 Docker 时代的 daemon.json,到 containerd 1.x 的 config.toml,再到 2.x 的 hosts.toml,最大的体会就是:containerd 的配置并没有变“复杂”,只是换了一套更灵活的机制,把仓库配置从“全局单文件”变成了“按仓库目录管理”。这种设计对大规模集群非常友好——你在每台节点上维护的其实是同一套仓库配置,可以用配置管理工具批量下发,而不是去改那个庞大的 config.toml。
有一个小技巧可以分享:在集群场景下,建议把 config_path = "/etc/containerd/certs.d" 这个目录纳入版本化管理,仓库的 hosts.toml 文件全部放到这个目录里。这样一来,节点初始化、扩缩容、更换仓库地址时,只需要同步这一小块内容,不用每次手工改主配置文件。
再提醒一句:配置镜像仓库时不要只盯着“能拉镜像”这个结果,还要考虑推镜像、认证、证书轮换、mirror 切换这些后续操作是否顺畅。镜像仓库配置的本质是给 containerd 建立一套清晰可信的“寻址规则”,规则清晰了,后面所有容器工作负载才会稳定。
