资讯详情

Kubernetes搭建Docker Registry私有仓库,containerd拉取镜像全攻略

📅 2026/10/20 17:28:47 | 华诺云谱 👁 阅读
Kubernetes搭建Docker Registry私有仓库,containerd拉取镜像全攻略
在 Kubernetes 集群里搭一个私有镜像仓库然后让 containerd 做运行时去拉镜像这个流程说简单不简单说难也不难但中间有几个细节一旦忽略轻则卡在 ImagePullBackOff重则把整个集群的镜像拉取链路搞得一团糟。这篇文章从实际部署角度把我在某内部项目里用 Docker Registry 2.7.1 搭建私有仓库、并且通过 containerd 成功拉取镜像到应用 Pod 的完整过程拆开讲一遍包括配置原因、操作命令、踩坑记录和验证方法。为什么选 Docker Registry 2.7.1而不是直接上 Harbor 或者 Nexus原因很直接这个版本稳定配置项清晰资源占用低在中小规模集群里完全够用。而且它支持 OCI Image Spec和 containerd 的配合也相对成熟。如果你只是想解决“内部镜像分发”这个单一问题Registry 2.7.1 是一个几乎不会出错的底子。1. 为什么要做“Registry containerd”这套组合先搞清楚这套架构在解决什么问题。Kubernetes 集群中镜像拉取由容器运行时负责。如果你的运行时是 Docker它默认从 Docker Hub 拉镜像如果是 containerd默认配置里也绑定了 Docker Hub 的镜像仓库地址。但在离线环境、内网环境、或者有合规要求的场景中你不可能每台节点都直连外网这时候就需要一个内网私有仓库作为镜像的中转站。有人会问直接用 Docker Hub 的私有仓库不就行了可以但内网环境访问外网不稳定而且带宽受限大规模发布时镜像拉取非常慢。私有仓库部署在集群内部节点到仓库走内网速度有保证也方便控制镜像版本和权限。另外一个关键点是 containerd 的配置方式。containerd 与 Docker 不一样它没有一个统一的 daemon.json 来配置镜像仓库。containerd 在拉取镜像时会根据镜像域名去查找对应配置默认支持/etc/containerd/certs.d目录下的hosts.toml文件或者通过registry.mirrors和registry.configs配置。很多人第一次用 containerd 会发现docker login那套方法完全失效其实就是因为没搞懂这套查找规则。所以整套部署的链路可以理解为应用 Pod请求拉取镜像kubelet把请求发给containerdcontainerd根据镜像地址里的域名比如registry.internal.example.com去匹配 hosts.toml 配置配置指向我们的私有仓库并提供了证书或认证信息containerd 从私有仓库拉取镜像交给 kubelet 创建 Pod理解这条链路之后后面所有配置项都不再是孤立的知识点而是这条链路里的一环。2. 部署 Registry 2.7.1 之前的环境梳理在动手之前我习惯先把环境想明白。你的 Kubernetes 集群是怎么装的kubelet 用的什么运行时节点上的 containerd 版本是多少这些会直接影响配置方式。我这次的操作环境如下Kubernetes 版本1.23.5容器运行时containerd 1.6.xRegistry 镜像版本registry:2.7.1仓库暴露方式NodePort端口 30075镜像仓库域名registry.internal.example.com你可能注意到我把域名写成了一个内网域名而不是用 IP。这是有讲究的containerd 的 hosts.toml 配置是以域名为单位区分的如果后面想加 TLS 证书也必须让证书里的 CN/SAN 和域名匹配。统一用一个内网域名后面少很多麻烦。在部署 Registry 之前先准备好数据目录和认证信息。Registry 2.7.1 默认会把镜像数据存储在容器的/var/lib/registry目录下生产环境建议用 PVC 挂出来。除此之外我建议你在一开始就把认证加上不要裸奔。裸奔的仓库在公网环境等于给自己埋雷即使在内网也保不齐有哪台机器被入侵后反向利用你的仓库。先创建认证文件。Registry 2.7.1 支持 htpasswd 方式做基础认证。你可以用下面命令生成一个用户admindocker run --rm --entrypoint htpasswd registry:2.7.1 -Bbn admin yourpassword htpasswd这里注意一点-B参数很重要它表示使用 bcrypt 算法加密密码。Registry 2.7.1 要求密码哈希必须能被golang.org/x/crypto/bcrypt解析所以不要用旧格式的 htpasswd 文件。我第一次图省事用系统自带的 htpasswd 命令-B参数在部分发行版里默认不带结果 Registry 启动时报认证文件格式错误。如果你所在环境没有 Docker CLI只有 Podman 或者 skopeo也可以这样podman run --rm --entrypoint htpasswd registry:2.7.1 -Bbn admin yourpassword htpasswd生成的 htpasswd 文件内容长这样admin:$2y$05$8GKZsuH26YJfYFf4y1hv5e...确认文件没问题后把它放到一台管理机上稍后会在创建 ConfigMap 时用到。3. 在 Kubernetes 中部署 Registry 2.7.1资源清单详解这一步我们不再用裸容器跑 Registry而是把它作为一个 Deployment 部署进集群。好处是能利用 Kubernetes 的调度和自愈能力后续扩容也方便。3.1 创建 Namespace 和认证 ConfigMap我们单独用一个 namespace 来隔离仓库服务避免和业务应用混在一起。kubectl create namespace registry接下来把 htpasswd 文件导入 ConfigMapkubectl create configmap registry-htpasswd --from-filehtpasswd -n registry这个 ConfigMap 后面会挂载到 Registry 容器里供其读取认证文件。注意 Registry 容器进程默认以 root 身份运行但如果你的集群开启了强制 runAsNonRoot那就要给镜像单独设置一个 UID并且确保 htpasswd 文件对这个 UID 可读。我这边因为集群没有强制限制直接使用默认用户省了不少事。3.2 PVC 准备镜像数据不能丢Registry 的镜像数据必须持久化否则 Pod 重建一次镜像就全没了。先建一个 PVC。假设你的存储类叫standard资源清单如下apiVersion: v1 kind: PersistentVolumeClaim metadata: name: registry-data namespace: registry spec: accessModes: - ReadWriteOnce storageClassName: standard resources: requests: storage: 50Gi如果只是测试环境你完全可以用 hostPath 或者 local PV。但要注意如果未来多副本部署ReadWriteOnce 的限制会比较尴尬需要切换为 ReadWriteMany 存储。把 PVC 创建好然后写 Deployment。3.3 Deployment 清单核心配置解读先贴出我使用的完整 Deployment 清单然后逐段解释重点apiVersion: apps/v1 kind: Deployment metadata: name: docker-registry namespace: registry spec: replicas: 1 selector: matchLabels: app: docker-registry template: metadata: labels: app: docker-registry spec: containers: - name: registry image: registry:2.7.1 ports: - containerPort: 5000 name: registry env: - name: REGISTRY_HTTP_ADDR value: 0.0.0.0:5000 - name: REGISTRY_STORAGE_DELETE_ENABLED value: true - name: REGISTRY_AUTH_HTPASSWD_REALM value: Registry Realm - name: REGISTRY_AUTH_HTPASSWD_PATH value: /auth/htpasswd - name: REGISTRY_LOG_LEVEL value: info volumeMounts: - name: data mountPath: /var/lib/registry - name: auth mountPath: /auth readOnly: true volumes: - name: data persistentVolumeClaim: claimName: registry-data - name: auth configMap: name: registry-htpasswd这个清单里的每个环境变量都有它存在的意义REGISTRY_HTTP_ADDRRegistry 默认监听 5000 端口这里显式声明避免因为环境变量缺省产生歧义。REGISTRY_STORAGE_DELETE_ENABLED允许删除镜像。Registry 2.7.1 默认不支持删除 API必须手动开启。REGISTRY_AUTH_HTPASSWD_REALM和REGISTRY_AUTH_HTPASSWD_PATH开启基础认证指向之前挂载的 htpasswd 文件。REGISTRY_LOG_LEVEL日志级别。调成info方便排查问题生产环境也可以维持这个级别Registry 的日志量不大。这里没配置 TLS是因为我准备在 containerd 端做私有证书信任仓库本身先不启用 HTTPS。如果你希望仓库直接启用 HTTPS那需要额外挂载证书文件并设置REGISTRY_HTTP_TLS_CERTIFICATE和REGISTRY_HTTP_TLS_KEY环境变量。不过这次为了演示 containerd 的hosts.toml对 HTTP 仓库的处理方式我在测试环境先让它跑 HTTP。提示在 containerd 里配置 HTTP 仓库时一定要在 hosts.toml 里显式声明skip_verify true或者http true否则 containerd 默认走向 HTTPS会造成connection refused或者证书验证失败。3.4 Service 暴露用 NodePort 还是 Ingress创建 Deployment 之后需要把服务的 5000 端口暴露出去。这里视你的集群类型而定如果节点是裸机NodePort 最直接。如果有 Ingress Controller可以让 Service 使用 ClusterIP然后 Ingress 转发。如果只是临时测试直接kubectl port-forward也行。我用 NodePort好处是任何节点都能通过节点IP:30075访问仓库。apiVersion: v1 kind: Service metadata: name: docker-registry namespace: registry spec: type: NodePort selector: app: docker-registry ports: - port: 5000 targetPort: 5000 nodePort: 30075创建完之后可以用任意节点 IP 去验证仓库是否存活curl -u admin:yourpassword http://node-ip:30075/v2/如果返回{}说明仓库已经正常响应。这一步非常关键后面 containerd 能不能拉取镜像都是建立在这个基础之上。4. containerd 侧配置让运行时找到你的仓库Registry 部署好了但 containerd 默认不知道你的私有仓库。你需要让 containerd 在拉取registry.internal.example.com/xxx镜像时把请求发到对应地址并且处理 HTTP/TLS 问题。4.1 containerd 配置文件的三个关键位置containerd 的配置入口首先是/etc/containerd/config.toml。在 1.6 版本里和私有仓库相关的字段主要是[plugins.io.containerd.grpc.v1.cri.registry]config_pathmirrorsconfigs网上很多老教程让你改mirrors和configs这些写法在 1.6 里依然能用但更推荐的方式是config_path。这个字段指向一个目录目录下面按照域名存放配置文件灵活度更高还可以 override 镜像的源地址也就是hosts.toml机制。先检查当前的 config.toml 里有没有config_pathgrep -n config_path /etc/containerd/config.toml如果没有手动加一行version 2 [plugins.io.containerd.grpc.v1.cri.registry] config_path /etc/containerd/certs.d修改之后执行systemctl restart containerd注意containerd 配置的config_path不是必须的但如果你不配置就只能在 config.toml 里写死镜像配置。一旦镜像地址变多维护成本很高。我建议从一开始就用 config_path。4.2 创建 hosts.toml核心理解点config_path指向/etc/containerd/certs.d之后containerd 会在这个目录里查找与镜像域名同名的子目录读取hosts.toml文件。假设我们要从registry.internal.example.com拉取镜像那么就要创建/etc/containerd/certs.d/registry.internal.example.com/hosts.toml内容如下server https://registry.internal.example.com:30075 [host.https://registry.internal.example.com:30075] capabilities [pull, resolve] skip_verify true等等这里有个问题。我们的仓库目前是 HTTP 的而server字段写的是https。containerd 会把https://registry.internal.example.com:30075当作默认 server但host下面的地址也会被访问。因为我这里为了让 containerd 不校验证书用了skip_verify true。可如果是纯 HTTP 仓库可以简化成server http://registry.internal.example.com:30075 [host.http://registry.internal.example.com:30075] capabilities [pull, resolve]如果你希望仓库地址和 Pod 内看到的镜像地址一致那么 Pod 的镜像名必须是registry.internal.example.com:30075/your-image:v1。这里hosts.toml里的server会和 hosts.toml 对应的域名绑定其实 containerd 会把registry.internal.example.com解析到server配置的地址去访问。如果 Pod 中写的是registry.internal.example.com:30075/your-image:v1那 hosts.toml 的目录名就必须是registry.internal.example.com:30075而不是registry.internal.example.com。这一点非常容易搞混。正确的做法是目录名要和镜像地址里的域名包括端口完全一致。我这次用的方式是Pod 镜像名写registry.internal.example.com:30075/hello-app:v1所以目录就建registry.internal.example.com:30075mkdir -p /etc/containerd/certs.d/registry.internal.example.com:30075然后 hosts.toml 内容server http://registry.internal.example.com:30075这样 containerd 一看到镜像是registry.internal.example.com:30075/xxx就会自动去访问http://registry.internal.example.com:30075不再走 Docker Hub 默认地址。如果你不加hosts.tomlcontainerd 会尝试直接访问https://registry.internal.example.com:30075/v2/失败后可能 fallback 到默认 Docker Hub 镜像地址然后报一个not found。这个现象很典型网上搜ErrImagePull经常会看到这个原因。4.3 把配置同步到所有节点Kubernetes 里的 Pod 可能调度到任意节点。如果只有一台节点配置了 hosts.toml其他节点照样拉不到镜像。所以要么用 Ansible 之类的工具批量推送要么写个 DaemonSet 去挂载配置。我这次的集群就三台节点直接写了个小脚本循环推送for node in node1 node2 node3; do ssh $node mkdir -p /etc/containerd/certs.d/registry.internal.example.com:30075 scp hosts.toml $node:/etc/containerd/certs.d/registry.internal.example.com:30075/hosts.toml ssh $node systemctl restart containerd done如果你节点很多建议用配置管理工具不要手动输命令。此外重启 containerd 会影响节点上正在运行的容器吗理论上 containerd 重启期间正在运行的容器不受影响因为容器进程由 shim 管理但 kubelet 和 containerd 之间的连接会短暂中断。大规模生产集群里尽量选择在变更窗口做或者采用逐节点滚动重启的方式。5. 让应用 Pod 从 Registry 拉取镜像并启动配置完 containerd 只是解决了“能不能连上仓库”的问题。接下来要面对两个问题认证和 Pod 声明。5.1 仓库认证Secret 与 imagePullSecrets因为 Registry 开启了 htpasswd 认证containerd 拉取镜像时也需要携带用户名密码。配置方法是在 Kubernetes 中创建一个dockerconfigjson类型的 Secret然后挂到应用 Pod 的imagePullSecrets字段。先创建 Secretkubectl create secret docker-registry registry-secret \ --docker-serverregistry.internal.example.com:30075 \ --docker-usernameadmin \ --docker-passwordyourpassword \ -n your-namespace如果直接用 kubectl 不方便也可以用 Docker config 文件的方式。生成一个config.json{ auths: { registry.internal.example.com:30075: { username: admin, password: yourpassword, auth: YWRtaW46eW91cnBhc3N3b3Jk } } }auth字段的值就是username:password的 base64。然后kubectl create secret generic registry-secret \ --from-file.dockerconfigjsonconfig.json \ --typekubernetes.io/dockerconfigjson \ -n your-namespace在很多排错场景里我发现最容易出问题的不是 Secret 本身而是--docker-server的值。它必须和镜像地址里的域名端口保持完全一致。如果你镜像写的是registry.internal.example.com:30075/app:v1那么 Secret 里的 server 也必须是registry.internal.example.com:30075写成registry.internal.example.com都不行。因为 containerd 会根据镜像地址的域名去找对应的认证配置域名对不上就直接走匿名访问返回unauthorized或no basic auth credentials。有些团队为了省事把 Secret 加到default这个 service account 上这样所有 Pod 默认都带这个私有仓库凭据kubectl patch serviceaccount default -n your-namespace \ -p {imagePullSecrets: [{name: registry-secret}]}这种方式适合一个 namespace 里绝大多数镜像都来自此仓库的情况。如果只是个别 Pod 需要直接在 Deployment 里写imagePullSecrets更清晰。5.2 编写测试应用 Deployment下面用一个最简单的 hello-world 镜像做验证。先把镜像推到私有仓库docker pull alpine:3.19 docker tag alpine:3.19 registry.internal.example.com:30075/test/alpine:3.19 docker push registry.internal.example.com:30075/test/alpine:3.19推送时输入 admin/yourpassword。如果推送失败先确认仓库 API 是否可达再看认证问题。然后创建应用 PodapiVersion: apps/v1 kind: Deployment metadata: name: test-alpine namespace: your-namespace spec: replicas: 1 selector: matchLabels: app: test-alpine template: metadata: labels: app: test-alpine spec: imagePullSecrets: - name: registry-secret containers: - name: alpine image: registry.internal.example.com:30075/test/alpine:3.19 command: [sleep, 3600]创建后查看状态kubectl get pods如果 Pod 状态是Running说明整个链路已经打通。此时你可以进到容器里验证kubectl exec -it test-alpine-xxxx -- cat /etc/os-release看到 Alpine 的版本信息就说明镜像确实被拉下来并成功启动。如果你的 Pod 一直停留在ImagePullBackOff先别慌按下面顺序排查。5.3 排错ImagePullBackOff 的常规链路ImagePullBackOff不是凶手它只是一个结果。真正的原因在 kubelet 的事件里。用下面命令查看kubectl describe pod test-alpine-xxxx常见错误大概有三类第一类x509 证书错误报错类似failed to resolve reference registry.internal.example.com:30075/test/alpine:3.19: failed to do request: x509: certificate signed by unknown authority这个问题的原因是 containerd 在访问 HTTPS 地址时不认识仓库的证书。解决办法有两种要么把 CA 证书加到系统信任库要么在 hosts.toml 里写skip_verify true。生产环境建议把 CA 证书加进节点信任库并重启 containerd。skip_verify只适合内网测试。第二类连接拒绝dial tcp 10.x.x.x:30075: connect: connection refused这说明 containerd 访问的地址不对或者 Service 没有正确暴露端口。需要检查 Service 的 nodePort 和所在节点防火墙。第三类401 Unauthorizedfailed to resolve reference ...: unexpected status code 401: Unauthorized说明认证信息没对上。检查imagePullSecrets的 Secret 名字、--docker-server 是否和镜像地址一致。在我实际排错过程中80% 的问题都出在“目录名和镜像域名不一致”和“Secret 的 docker-server 不匹配”这两个点上。你把这两项一改对基本就通了。6. 进阶配置用 HTTPS 和带认证的仓库提升安全等级上面这套流程能跑通但仓库还是 HTTP凭据也是明文传输这在跨网段环境里会有安全隐患。更稳妥的配置是给 Registry 挂上自签证书然后在 containerd 里信任这个 CA。6.1 生成自签证书我用 openssl 生成一个自签 CA再签一个服务端证书证书的 SAN 里必须包含registry.internal.example.comopenssl req -new -x509 -days 3650 -nodes -out ca.crt -keyout ca.key -subj /CNinternal-ca openssl req -new -out server.csr -keyout server.key -subj /CNregistry.internal.example.com \ -addext subjectAltNameDNS:registry.internal.example.com openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \ -out server.crt -days 730 -extfile (printf subjectAltNameDNS:registry.internal.example.com)然后把server.crt和server.key挂到 Registry Deployment 中。6.2 Registry 启用 TLSDeployment 添加两个环境变量env: - name: REGISTRY_HTTP_TLS_CERTIFICATE value: /certs/server.crt - name: REGISTRY_HTTP_TLS_KEY value: /certs/server.key并挂载对应 volume。重启后用curl -u admin:yourpassword https://registry.internal.example.com:30075/v2/验证。6.3 containerd 添加 CA 信任此时 containerd 的 hosts.toml 不再需要skip_verify true而是配置ca路径server https://registry.internal.example.com:30075 [host.https://registry.internal.example.com:30075] capabilities [pull, resolve] ca /etc/containerd/certs.d/registry.internal.example.com:30075/ca.crt然后把ca.crt复制到该目录下重启 containerd。这样 containerd 就会用我们的自签 CA 去验证仓库证书链路是可信的。6.4 清理和备份Registry 的运维基本功镜像会越堆越多。Registry 2.7.1 默认不支持通过 API 删除镜像层需要先启用REGISTRY_STORAGE_DELETE_ENABLEDtrue再用垃圾回收命令/bin/registry garbage-collect /etc/docker/registry/config.yml另外Registry 的存储目录是重要的数据资产建议定期把 PVC 里的数据备份到对象存储或者其他存储。备份方式不复杂直接把/var/lib/registry/docker/registry/v2目录打包拷走即可。镜像不清理磁盘迟早会满。我这边的经验是给 Registry 的 PVC 加一个监控项使用达到 70% 就告警到了 85% 就强制清理。别等磁盘满了再手动处理那时候 kubelet 的 eviction 机制会先踢掉一堆 Pod波及面很大。7. 我踩过最多的三个坑最后集中聊一下这几个操作的隐藏坑。这些坑在文档里不会重点标出但对线排查时十有八九会遇到。第一个坑改完 hosts.toml 后忘记重启 containerd或者重启方式不对。在部分发行版里systemctl restart containerd会等较长时间因为 containerd 要优雅退出。如果等得不耐烦直接kill -9可能导致节点上已有的容器短暂失联。建议用systemctl reload containerd吗实测下来 containerd 的 reload 并不总是生效于 hosts.toml 变更所以我更推荐直接 restart但要选业务低峰期。第二个坑节点上有别的镜像仓库配置覆盖了 hosts.toml。如果你同时用多个私有仓库每个仓库都要建对应的目录。有些镜像源软件会在安装时帮你生成/etc/containerd/certs.d/docker.io/hosts.toml里面的server可能被改成了国内镜像源。这时候如果你拉 Docker Hub 官方镜像会走那个 mirror 配置和本文章讨论的 Registry 没有冲突但会让你误以为 hosts.toml 没生效。排查时先看 containerd 日志确认它实际访问的地址是什么。第三个坑Secret 的自动更新问题。使用kubectl create secret docker-registry创建的 Secret 是一次性提交密码过期后不会自动同步。很多团队会写一个 CronJob 定期更新这个 Secret这是合理的做法但是要注意更新后必须重启相关 Pod 才会重新读取kubelet 不会自动刷新已运行的 Pod 的 imagePullSecret。这套链路跑顺之后后续往私有仓库推送镜像、发布版本、回滚版本都很顺畅。containerd 的 hosts.toml 机制相比传统配置更直观只要记住“目录名 镜像地址的域名:端口”这条红线基本不会再出方向性错误。Registry 2.7.1 虽然不算新版本但作为内部基础组件它足够稳定配合 containerd 1.6 完全能满足生产环境的核心镜像分发需求。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑