1. 项目概述:容器镜像仓库不是“网盘”,而是精密协作中枢
你有没有在终端里敲下
docker push
后,盯着那一行不断滚动的
pushed
提示发过呆?或者在 CI/CD 流水线卡在
pulling from registry
时,反复刷新日志却只看到进度条原地打转?很多人把容器 registry(镜像仓库)当成一个“高级网盘”——上传镜像叫 push,下载镜像叫 pull,点几下就完事。但真实情况远比这复杂得多:它既不是 FTP 服务器,也不是对象存储桶的简单封装,而是一套融合了内容寻址、分层校验、并发控制、权限策略与元数据治理的分布式协作基础设施。我从 2015 年开始搭建私有 Harbor,到后来在生产环境维护支持 300+ 微服务、日均 2000+ 次 push/pull 的 Artifactory 集群,再到最近用 nerdctl + containerd 深度调试一个离线边缘节点的拉取失败问题,才真正意识到——
push 和 pull 这两个看似原子的操作,背后各自触发了至少 12 个关键子流程,涉及 7 类协议交互、4 层校验机制和 3 种状态同步模型
。本文不讲 Docker Desktop 图形界面怎么点,也不复述
docker login
命令怎么写;我要带你钻进 registry 的腹地,看清楚当
nerdctl push localhost:5000/app:latest
执行时,网络包在哪些进程间跳转、SHA256 哈希值如何被逐层验证、manifest 文件怎样被动态组装、blob 数据又为何能实现跨镜像去重。适合正在排查
unauthorized: authentication required
却连 auth server 地址都找不到的运维同学,也适合刚写完第一个 Dockerfile、却对
FROM alpine:3.18
底层如何加载一无所知的开发者。你不需要会写 Go,但得愿意跟着 curl 和 tcpdump 一起“抓包”。
2. 核心设计逻辑:为什么 registry 必须是“内容寻址”而非“路径寻址”
2.1 传统文件服务器思维的致命陷阱
先破一个常见误区:很多初学者尝试用 Nginx 搭建“简易 registry”,把 tar.gz 镜像包扔进
/var/www/html/images/
,再配个
location /images/ { alias /var/www/html/images/; }
,以为这样就能
curl http://localhost/images/nginx.tar.gz
下载镜像。这完全走错了方向。问题出在
语义错位
——Docker 镜像不是单个文件,而是一个由 manifest(清单)、config(配置)、layer(层)三类 blob 构成的有向无环图(DAG)。比如一个典型的
nginx:alpine
镜像,实际包含:
- 1 个 manifest 文件(JSON 格式),声明该镜像由哪些 layer 组成、各 layer 的 digest 是什么、config blob 的 digest 是什么;
- 1 个 config blob(JSON),记录构建时间、环境变量、入口命令等运行时元数据;
-
3~5 个 layer blobs(二进制 tar.gz),每个对应 Dockerfile 中一条
RUN或COPY指令产生的文件系统变更。
如果用路径寻址(如
/images/nginx/alpine/manifest.json
),你必须保证所有客户端都严格遵循同一套目录命名规则,且每次
docker build
生成的 layer digest 必须可预测——这显然不可能,因为 layer digest 是对压缩后 tar 包内容做 SHA256 计算得出,哪怕文件时间戳差 1 秒,digest 就完全不同。我曾见过团队用 Jenkins 构建脚本硬编码
cp nginx-manifest.json /registry/nginx/manifest.json
,结果上线后因基础镜像更新导致 layer digest 不匹配,整个集群拉取失败,排查三天才发现是 manifest 里写的 digest 和实际上传的 blob digest 对不上。
2.2 内容寻址(Content Addressing)如何解决一致性难题
Registry 的核心设计哲学是
一切以内容哈希为唯一标识
。当你执行
docker push my-registry.com/app:1.0
时,客户端不会直接上传整个镜像,而是:
-
本地解析镜像结构
:
docker image inspect app:1.0获取 manifest、config、layer 的本地路径; -
计算各 blob 的 digest
:对每个文件做
sha256sum,得到形如sha256:abc123...的字符串; -
按 digest 路径组织上传
:将 manifest 上传至
/v2/app/manifests/sha256:xyz789...,layer 上传至/v2/app/blobs/sha256:abc123...,config 上传至/v2/app/blobs/sha256:def456...; -
最后提交 tag 映射
:向
/v2/app/tags/list发送 PUT 请求,将1.0这个 tag 指向 manifest 的 digest。
这个设计带来三个关键优势:
-
强一致性保障
:任何客户端只要拿到 manifest digest,就能精确获取该镜像的完整内容快照,不受 tag 覆盖影响。比如你
docker pull my-registry.com/app:1.0,registry 返回的 manifest 里明确写着"config": {"digest": "sha256:def456..."},客户端就知道必须拉取这个 exact digest 的 config blob,而不是“最新版的 config”。 -
跨镜像层共享
:两个不同镜像(如
app:1.0和app:1.1)若使用相同基础层(如alpine:3.18的 rootfs layer),它们的 layer blob digest 完全一致,registry 只需存储一份物理副本,节省 60%+ 存储空间。我们线上集群统计显示,平均每个 layer blob 被 3.7 个不同镜像引用。 -
防篡改验证
:客户端拉取 blob 后,会重新计算其 SHA256 值,并与 manifest 中声明的 digest 比对。不一致则立即报错
failed to verify sha256,杜绝中间人篡改风险。
提示:你可以用
skopeo inspect docker://docker.io/library/alpine:3.18查看远程镜像的 manifest 结构,重点关注layers[].digest和config.digest字段。对比skopeo copy拉取后本地cat /var/lib/containers/storage/overlay-images/.../manifest的内容,你会发现 digest 完全一致——这就是内容寻址的实证。
2.3 分布式场景下的状态同步挑战
当 registry 部署为多节点集群(如 Harbor HA 模式或自研基于 S3 的分片 registry),问题升级为
跨节点状态同步
。假设节点 A 接收
push
请求并成功写入 manifest 和 layer blob,但节点 B 的缓存尚未更新。此时客户端向节点 B 发起
pull
,B 可能返回
404 Not Found
,因为它本地没查到该 manifest digest 对应的元数据。解决方案不是简单加 Redis 缓存,而是引入
最终一致性协议
:
- Manifest 元数据采用强一致存储 :Harbor 使用 PostgreSQL 存储 repository/tag/manifest 关系,所有写操作走事务,确保 tag 到 manifest digest 的映射绝对准确;
-
Blob 数据采用弱一致对象存储
:S3/Blob Storage 本身无事务,但通过
HEAD请求预检 blob 是否存在(curl -I https://s3-bucket/abc123...),配合指数退避重试,容忍短暂延迟; -
客户端主动降级策略
:nerdctl 在 pull 失败时,会自动尝试向 registry 的
/v2/_catalog端点查询所有仓库列表,再遍历已知仓库检查 manifest 是否存在,避免因单点故障导致整个拉取链路中断。
这解释了为什么你在
docker pull
时偶尔看到
retrying in 1s...
日志——这不是网络抖动,而是客户端在执行分布式状态收敛的主动等待。
3. Push 操作深度拆解:从本地镜像到远程存储的 7 个关键阶段
3.1 阶段一:认证与作用域协商(Auth Flow)
docker push
的第一行日志永远是
The push refers to repository [my-registry.com/app]
,但这行之前,客户端已静默完成认证。过程如下:
-
客户端向 registry 的
/v2/端点发送匿名 GET 请求; -
registry 返回
401 Unauthorized,并在WWW-AuthenticateHeader 中携带认证信息,例如:WWW-Authenticate: Bearer realm="https://my-registry.com/auth",service="my-registry.com",scope="repository:app:push,pull" -
客户端提取
realmURL(这里是https://my-registry.com/auth),向其发送 POST 请求,附上 base64 编码的用户名密码(docker login时保存的凭证); -
Auth Server 验证凭据后,返回 JWT Token,其中
scope字段明确限定本次 token 仅可用于app仓库的push和pull操作; -
客户端用此 Token 重新发起 push 请求,Header 中带上
Authorization: Bearer <token>。
关键点在于
scope
的粒度控制。企业级 registry(如 Harbor)支持更细粒度的 scope,例如
repository:prod/app:push
(仅允许推送 prod 环境的 app 镜像)或
registry:catalog:*
(允许浏览所有仓库)。我曾遇到一个安全审计案例:某团队误将
scope="registry:*"
的 token 硬编码在 CI 脚本中,导致攻击者获取 token 后可
curl -H "Authorization: Bearer xxx" https://registry.com/v2/_catalog?n=1000
列出全部 2000+ 个私有仓库名,暴露敏感业务架构。
注意:nerdctl 默认使用 containerd 的 credential helper(如
pass或gcr),其认证流程与 Docker CLI 略有不同——它会先调用containerd的GetCredentials接口获取 token,再注入到 HTTP 请求中。调试时可用nerdctl --debug push ...查看详细认证日志。
3.2 阶段二:Manifest 预检与上传(Manifest Upload)
认证通过后,客户端开始上传 manifest。但这里有个反直觉设计: manifest 本身不包含任何二进制数据,它只是一个纯 JSON 描述文件 。因此上传速度极快,通常毫秒级完成。然而,这步失败率却很高,原因在于 registry 对 manifest 的 schema 有严格校验:
-
必须包含
schemaVersion: 2字段; -
mediaType必须为application/vnd.docker.distribution.manifest.v2+json(OCI 镜像则为application/vnd.oci.image.manifest.v1+json); -
layers[]数组中每个元素的digest必须符合sha256:<64 hex chars>格式; -
config.digest必须与后续上传的 config blob digest 完全一致。
我在线上遇到过最隐蔽的坑:某团队用自研脚本生成 manifest JSON,但
digest
字段末尾多了个空格(
"sha256:abc123... "
),registry 解析时认为这是非法 digest,返回
400 Bad Request
,错误信息却是
invalid manifest: invalid reference format
,完全没提空格问题。最终用
jq -r '.layers[0].digest' manifest.json | od -c
查看十六进制编码才定位到空格字符。
3.3 阶段三:Layer Blob 分块上传(Chunked Upload)
真正的耗时大户是 layer blob 上传。Docker 客户端采用 分块上传(chunked upload) 机制,而非一次性 POST 整个几百 MB 的 tar.gz。流程如下:
-
客户端向
/v2/app/blobs/uploads/发送 POST 请求,registry 返回一个唯一的upload UUID和locationURL(如/v2/app/blobs/uploads/abc123...); -
客户端将 layer 文件切分为多个 5MB 的 chunk(最后一块可能更小),依次向
locationURL 发送 PATCH 请求,Body 为 chunk 二进制数据,Header 中Content-Range: 0-5242879/123456789声明当前块偏移量; -
所有 chunk 上传完成后,客户端向
locationURL 发送 PUT 请求,Body 为空,Header 中Digest: sha256:abc123...声明完整 blob 的 digest; -
registry 校验所有 chunk 拼接后的 SHA256 是否与
DigestHeader 一致,一致则合并存储,返回201 Created。
这种设计的好处是
断点续传
。如果网络中断,客户端可向
location
URL 发送 HEAD 请求,registry 返回
Range: bytes=0-5242879
,告知已接收的字节数,客户端即可从断点继续上传。我们曾用
tcpkill -i eth0 port 443
模拟网络闪断,验证了 nerdctl 在 3 次重试后成功续传 2.3GB 的 CUDA 镜像 layer。
3.4 阶段四:Config Blob 上传与关联
Config blob 是镜像的“身份证”,体积小(通常 2~5KB),但语义关键。它包含:
-
created: 镜像构建时间(ISO8601 格式); -
author: 构建者信息; -
config: 运行时配置,如Cmd,Env,ExposedPorts; -
rootfs: 声明该镜像由哪些 layer 组成(diff_ids数组,每个 diff_id 是未压缩 layer 的 SHA256)。
注意
diff_ids
与 manifest 中
layers[].digest
的区别:前者是未压缩 tar 的哈希,后者是压缩后 tar.gz 的哈希。registry 在收到 config blob 后,会将其
digest
写入数据库,并与 manifest 中的
config.digest
字段关联。如果两者不匹配,后续 pull 时客户端无法验证 config 完整性。
3.5 阶段五:Tag 映射创建(Tagging)
当所有 blob(manifest, config, layers)上传成功,客户端向
/v2/app/tags/list
发送 PUT 请求,Body 为 JSON:
{"name":"app","tags":["1.0"]}
registry 此时执行原子操作:在数据库中创建
repository_id -> tag_name -> manifest_digest
的三元组映射。这一步失败会导致
push
显示成功,但
pull
找不到 tag——因为 manifest 已存,只是 tag 没绑定。Harbor 的日志中会出现
tag not found for repository
错误。解决方案是手动调用 API 创建 tag:
curl -X PUT https://harbor/api/v2.0/projects/app/repositories/app/artifacts/sha256:xyz789.../tags/1.0
。
3.6 阶段六:存储后端写入确认
对于基于文件系统的 registry(如开源 distribution),blob 数据直接写入磁盘;对于对象存储后端(S3/OSS),registry 调用云厂商 SDK 上传对象。关键点在于
写入确认时机
:registry 必须在对象存储返回
200 OK
且校验 ETag(S3 的 MD5)与预期一致后,才向客户端返回成功。否则可能出现“客户端收到 push 成功,但实际 blob 未写入”的脑裂状态。我们曾因 S3 限流导致
PutObject
超时,registry 误判为失败,但部分 chunk 实际已写入,造成存储碎片。解决方案是启用 S3 的
ServerSideEncryption
和
ChecksumAlgorithm: SHA256
,强制服务端校验。
3.7 阶段七:客户端本地缓存更新
Push 完成后,客户端并非立刻清除本地缓存。Docker CLI 会更新
~/.docker/image/.../imagedb/content-sha256/
下的 manifest 和 config 文件,并在
~/.docker/registry/config.json
中记录 registry 的认证状态。nerdctl 则更新 containerd 的 content store(
/var/lib/containerd/io.containerd.content.v1.content/
)。这解释了为什么
docker system prune -a
会删除本地所有镜像,但 registry 中的数据完好无损——它们是完全独立的存储域。
4. Pull 操作深度拆解:从远程存储到本地运行的 8 个关键阶段
4.1 阶段一:Tag 解析与 Manifest 获取(Tag Resolution)
docker pull my-registry.com/app:1.0
的第一步,是将
1.0
这个 tag 解析为具体的 manifest digest。客户端向
/v2/app/manifests/1.0
发送 GET 请求(注意:不是
/v2/app/tags/1.0
)。registry 收到请求后:
-
查询数据库,找到
app仓库下1.0tag 对应的 manifest digest(如sha256:xyz789...); - 从存储后端读取该 digest 对应的 manifest JSON 文件;
-
返回 manifest,并在
Docker-Content-DigestHeader 中重复声明该 digest。
这里的关键是
tag 到 digest 的映射是 registry 维护的,而非客户端缓存
。所以即使你本地
docker images
没有
app:1.0
,只要 registry 有该 tag,就能拉取。但若 registry 返回
404
,说明 tag 不存在,此时
docker pull
会报错
manifest unknown
。注意:
1.0
是 tag,
sha256:xyz789...
才是内容地址,tag 可以被覆盖(
docker push
同名 tag 会替换旧 manifest),但 digest 永远不变。
4.2 阶段二:Manifest 校验与解析(Manifest Validation)
客户端收到 manifest 后,立即执行两重校验:
-
Header 校验
:比对响应 Header 中的
Docker-Content-Digest与 manifest JSON 内部的config.digest和layers[].digest字段是否格式合法; -
内容校验
:对 manifest JSON 字符串整体做 SHA256 计算,与
Docker-Content-Digest比对。这一步防止中间代理篡改 manifest 内容(如恶意替换layers[0].digest指向一个带后门的 layer)。
我曾用 mitmproxy 拦截 pull 请求,将 manifest 中某个 layer digest 改为另一个已存在的恶意 digest,结果
docker pull
直接失败,报错
failed to verify sha256 of manifest
。这就是内容寻址的安全价值。
4.3 阶段三:Config Blob 并行拉取(Config Fetch)
Manifest 解析完成后,客户端知道
config.digest
,于是向
/v2/app/blobs/sha256:def456...
发送 GET 请求拉取 config blob。由于 config 体积小,此步骤通常在 100ms 内完成。客户端会将 config 写入本地存储(Docker 为
~/.docker/image/.../imagedb/content-sha256/def456...
,nerdctl 为 containerd content store),并解析其
rootfs.diff_ids
数组,准备拉取对应的 layer。
4.4 阶段四:Layer Blob 并发下载(Concurrent Layer Fetch)
这才是 pull 的性能瓶颈所在。客户端根据 manifest 中
layers[]
的顺序,并发拉取所有 layer blobs。默认并发数:
-
Docker CLI:3 个连接(可通过
--max-concurrent-downloads调整); - nerdctl:5 个连接(containerd 默认)。
每个 layer 请求发送到
/v2/app/blobs/sha256:abc123...
,registry 返回
200 OK
和 tar.gz 数据流。客户端边下载边解压(streaming decompress),直接写入本地存储的 overlay 文件系统。这里有个重要优化:
客户端会检查本地是否已有该 digest 的 layer
。如果
~/.docker/overlay2/
下存在
l/abc123...
目录,就跳过下载,直接复用——这就是为什么
docker pull
后
docker run
启动极快,因为 layer 已就位。
4.5 阶段五:Layer 校验与去重(Layer Verification)
下载完成后,客户端对每个 layer tar.gz 文件重新计算 SHA256,并与 manifest 中声明的 digest 比对。不一致则报错
failed to verify sha256 of layer
。同时,客户端会计算该 layer 未压缩状态的 diff_id(即对 tar 文件做 SHA256),并与 config 中
rootfs.diff_ids
比对。只有两者都匹配,才认为 layer 加载成功。
实操心得:当 pull 失败报
error: retries exceeded pull时,不要急着重试。先用curl -I https://registry.com/v2/app/blobs/sha256:abc123...检查该 blob 是否存在(HTTP 200 表示存在,404 表示缺失)。如果存在,再用curl https://registry.com/v2/app/blobs/sha256:abc123... | sha256sum手动校验 digest。我们曾发现 CDN 缓存了损坏的 layer,导致所有客户端校验失败,清空 CDN 缓存后问题解决。
4.6 阶段六:镜像元数据组装(Image Metadata Assembly)
所有 blobs 拉取并校验成功后,客户端开始组装本地镜像元数据:
-
创建
imageID:对 manifest JSON 做 SHA256,作为该镜像的唯一 ID(Docker 中docker images显示的 IMAGE ID); -
生成
layer chain:按 manifest 中layers[]顺序,将每个 layer 的diff_id写入~/.docker/image/.../layerdb/sha256/.../cache-id,形成层叠关系; -
构建
graph driver映射:Docker 的 overlay2 驱动会为每个 layer 创建l/xxx符号链接,指向diff/xxx目录;nerdctl 的 containerd 则在io.containerd.snapshotter.v1.overlayfs中注册 snapshot。
这一步完成后,
docker images
才能列出该镜像,
docker history app:1.0
才能显示各层信息。
4.7 阶段七:本地存储优化(Storage Optimization)
Pull 完成后,客户端会执行后台优化:
-
硬链接去重
:如果多个镜像共享同一 layer digest,Docker 会在
overlay2/l/下为该 digest 创建唯一目录,并用硬链接指向它,避免磁盘重复存储; -
缓存清理
:Docker 默认保留最近 3 次 pull 的临时文件,超过则自动清理
~/.docker/tmp/; -
索引更新
:更新
~/.docker/image/.../repositories.json,将my-registry.com/app:1.0映射到刚生成的imageID。
4.8 阶段八:运行时准备(Runtime Preparation)
最后一步,当用户执行
docker run app:1.0
时,Docker daemon 会:
- 从本地存储加载 manifest 和 config;
-
按
layers[]顺序挂载 overlay2 文件系统(lowerdir=layer1:layer2:..., upperdir=writeable, workdir=work); -
用 config 中的
Cmd和Env启动容器进程。
整个过程无需再次访问 registry——pull 已将所有必要内容下载到本地。这也是为什么离线环境也能运行容器,只要镜像已 pull 完毕。
5. 工具链实战:Docker、nerdctl 与 registry 的行为差异详解
5.1 Docker CLI 的“黑盒”行为与调试技巧
Docker CLI 对 push/pull 过程做了大量封装,导致问题排查困难。例如:
-
隐藏重试逻辑
:当
docker push遇到网络超时,CLI 默认重试 5 次,间隔 1s、2s、4s、8s、16s,但日志只显示retrying in Xs...,不告诉你具体失败原因; -
证书处理不透明
:自签名 registry 时,Docker 要求将 CA 证书放入
/etc/docker/certs.d/my-registry.com:5000/ca.crt,但错误提示却是x509: certificate signed by unknown authority,新手常误以为要改 Docker daemon 配置; -
缓存干扰
:Docker 会缓存 registry 的 auth token,有效期 24 小时。若 token 过期,
docker push可能报unauthorized,但docker login显示已登录,需手动docker logout && docker login刷新。
调试建议:
-
开启 debug 模式:
docker --debug push ...,查看完整 HTTP 请求/响应; -
抓包分析:
sudo tcpdump -i any -w docker.pcap port 443,用 Wireshark 过滤http.request.uri contains "v2"; -
直接调用 registry API:
curl -H "Authorization: Bearer $TOKEN" https://registry.com/v2/app/manifests/sha256:xyz789...。
5.2 nerdctl 的“透明化”设计与 containerd 深度集成
nerdctl 作为 containerd 的原生 CLI,其 push/pull 行为更贴近底层,调试更直接:
-
认证委托给 containerd
:nerdctl 不自己管理 token,而是调用 containerd 的
GetCredentials接口,因此nerdctl login实际是配置 containerd 的 credential helper; -
日志更详细
:
nerdctl --debug push ...会打印每一步的 HTTP 方法、URL、状态码及 body 片段; -
支持 OCI Image Spec v1.1
:nerdctl 默认生成 OCI 格式 manifest(
application/vnd.oci.image.manifest.v1+json),而 Docker CLI 仍用 Docker Schema v2(application/vnd.docker.distribution.manifest.v2+json),两者不兼容。若 registry 只支持 Docker Schema,nerdctl push 会失败,需加--platform linux/amd64 --oci参数强制转换。
实测对比:在拉取同一个 1.2GB 的
python:3.9-slim
镜像时,nerdctl 平均耗时 42s,Docker CLI 为 58s。差距主要来自 nerdctl 的并发下载数更高(5 vs 3)且无额外的 daemon 通信开销。
5.3 Registry 选型关键参数对比表
| 特性 | Docker Distribution (开源) | Harbor | JFrog Artifactory | 自研 S3 Registry |
|---|---|---|---|---|
| 认证方式 | Basic Auth / Token | LDAP/AD/OIDC/DB | LDAP/AD/OIDC/SCIM | 自定义 OAuth2 |
| 存储后端 | Filesystem / S3 / Azure / GCS | S3 / OSS / NFS | S3 / NFS / DB | S3 / MinIO |
| GC 机制 |
手动
registry garbage-collect
| Web UI 一键 GC | Scheduled GC Job | Lambda 自动触发 |
| 镜像扫描 | 无 | 集成 Trivy/Clair | 集成 Xray | 自研 CVE 匹配引擎 |
| 高可用 | 需外部负载均衡 + 共享存储 | PostgreSQL + Redis + Shared Storage | Replicated DB + Shared Storage | S3 强一致性 + API 网关 |
| 调试难度 | 日志分散(registry.log + storage.log) | Harbor Portal 日志中心 | Artifactory UI 实时日志 | CloudWatch Logs + OpenTelemetry |
选择建议:
- 中小团队快速启动 :用 Docker Distribution + S3,5 分钟部署,成本最低;
- 企业级合规需求 :Harbor 是事实标准,LDAP 集成、审计日志、漏洞扫描开箱即用;
- 超大规模(>10k 镜像/天) :Artifactory 性能更稳,但 license 成本高;
-
边缘/离线场景
:自研 registry 必须支持
offline mode,即 pull 时若 registry 不可达,自动 fallback 到本地 cache。
5.4 常见错误代码速查与根因分析
| 错误信息 | HTTP 状态码 | 根因分析 | 解决方案 |
|---|---|---|---|
unauthorized: authentication required
| 401 | Token 过期或 scope 不足 |
docker logout && docker login
;检查 registry auth server 日志
|
manifest unknown
| 404 | Tag 不存在或拼写错误 |
curl -X GET https://registry.com/v2/app/tags/list
查看有效 tag
|
blob unknown to registry
| 404 | Manifest 中声明的 layer digest 在 registry 中不存在 |
检查 push 日志,确认所有 layer 是否上传成功;用
skopeo list
验证
|
failed to verify sha256 of layer
| — | 下载的 layer 与 manifest 声明的 digest 不匹配 |
清理本地缓存
rm -rf ~/.docker/overlay2/l/abc123...
;检查 registry 存储完整性
|
received unexpected HTTP status: 502 Bad Gateway
| 502 | registry 前端(Nginx)与 backend(registry service)网络不通 |
检查 Nginx upstream 配置;
curl http://localhost:5000/v2/
测试 backend
|
error: src refspec master does not match any
| — | 与 Git 混淆!这是 Git 错误,非 registry 问题 |
git branch -a
查看远程分支名,通常为
main
而非
master
|
注意:
error: retries exceeded pull不是 registry 返回的错误,而是客户端(Docker/nerdctl)内部超时。根源通常是网络丢包、DNS 解析慢或 registry 后端响应超时。用ping registry.com和dig registry.com排查基础网络,再用curl -v https://registry.com/v2/测试 HTTPS 连通性。
6. 生产环境避坑指南:从配置到监控的 12 个血泪教训
6.1 配置篇:那些让你深夜被 call 的参数
-
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY权限陷阱 :Docker Distribution 默认将 blob 存入/var/lib/registry,但若你用chown 1001:1001 /var/lib/registry修改属主,而 registry 容器以root用户运行,会导致permission denied。正确做法是启动容器时指定--user 1001:1001,或保持目录属主为root,让 registry 进程自行 drop privileges。 -
S3 存储的
region必填项 :用 AWS S3 作后端时,REGISTRY_STORAGE_S3_REGION必须显式设置(如us-east-1),否则某些区域(如cn-north-1)会返回400 Bad Request,错误信息却是invalid region。我们曾因此在阿里云北京区部署失败,折腾两天才发现是 region 配置缺失。 -
REGISTRY_HTTP_ADDR的端口暴露 :registry 默认监听:5000,但若你用docker run -p 80:5000暴露 HTTP,而客户端用https://registry.com访问,会因 TLS 重定向失败。务必统一协议:要么全 HTTP(不推荐),要么用 Nginx 反向代理加 TLS。
6.2 网络篇:防火墙与代理的隐形杀手
-
企业防火墙拦截
CONNECT方法 :某些金融企业防火墙会拦截 HTTPS CONNECT 隧道,导致docker push卡在Waiting for registry。解决方案是配置 Docker daemon 的http-proxy,或联系网络团队放行CONNECT。 -
Nginx 代理的
client_max_body_size:registry 上传 layer 时可能达 2GB,Nginx 默认client_max_body_size 1m会直接返回413 Request Entity Too Large。必须在 proxy 配置中设为0(无限制)或足够大值。 -
DNS 缓存污染 :Kubernetes 集群内
coredns缓存了 registry 域名的旧 IP,导致pull请求发往已下线的节点。解决方案是设置ttl 30并定期kubectl exec coredns -- kill -SIGUSR1 1刷新缓存。
6.3 存储篇:磁盘爆满与数据丢失的预警
-
registry garbage-collect不清理 dangling blob :GC 命令只删除无 manifest 引用的 blob,但若 manifest 被 tag 引用,即使该 tag 已删除(如docker tag -d),blob 仍保留。需先registry delete删除 tag,再 GC。我们曾因未定期清理,3TB 磁盘在 2 个月内被占满。 -
S3 存储的
listObjectsV2权限缺失 :Harbor 同步仓库列表时需s3:ListBucket权限,若 IAM Policy 没授予,Harbor UI 显示Failed to fetch repositories,日志却是AccessDenied。务必在 S3 Policy 中添加"Action": "s3:ListBucket"。

385

被折叠的 条评论
为什么被折叠?



