
下午三点,二十几个 CI 任务挤在同一条出口 IP 上拉基础镜像,Docker Hub 甩回 429(Too Many Requests),流水线整批卡住,等下一个六小时窗口。这不是段子,Docker Hub 的限流政策白纸黑字写在官方 usage 文档里:匿名用户每 6 小时 100 次 pull,按 IPv4 地址或 IPv6 /64 子网计;认证的免费个人账号 200 次;付费的 Pro/Team/Business 才不限次。另外 429 也不全是一个来路:配额耗尽的拉取限流之外,还有并发过猛触发的滥用限流,后者连付费账号都不豁免。付费确实能解锁配额,但有两个问题它管不了:所有构建机的拉取流量都要出境走一遍 Hub,快慢看网络脸色;公司自己的私有镜像,也总得有个家。
这篇讲团队侧的解法:在内网自建一台 Docker Registry,存储后端接 RustFS,推镜像、拉镜像全部留在内网,配额这件事由你自己的磁盘说了算。个人开发者想省点拉取流量的话,那是另一条路(daemon 的 registry-mirrors,见 Distribution 官方 mirror 配方),本文不展开。

分工:谁管分发,谁管存放
Registry 本体用 Docker 官方开源的 Distribution,也就是 registry 镜像背后的项目,Apache 2.0 协议。它的存储层是插件式的:本地文件系统、S3、Azure 都能接。我们选 S3 这条路,把镜像 blob(构成镜像分层的数据块)全部放进对象存储,registry 容器本身变成无状态的,备份、扩容、容量管理都归存储层管,镜像仓库和生产数据不再抢同一块盘。
存储后端用 RustFS,一个面向 AI 时代、从零原生打造的高性能分布式对象存储,100% S3 兼容、Apache 2.0 开源。如果团队已经有一套 S3 兼容存储在跑,选 S3 后端等于把镜像仓库的运维并进现有的容量、备份、监控体系里;filesystem 后端当然也能用,但那意味着 registry 要自己挂一块专属盘,备份和扩容都得单独操心。我们的目标就是前者。S3 兼容在这里是硬指标:Distribution 的 S3 驱动并不认识 RustFS,它只管对着一个 S3 端点说标准的话,端点背后是谁它不关心。
前置条件:
- 一台内网机器,装好 Docker Engine;
- RustFS 已部署,没装的话按官方 Docker 安装文档起一个单节点,评估够用;
- 任意一台能跑 AWS CLI 的机器,用来建桶和检查数据。
先给 Registry 备好桶和密钥
RustFS 的 S3 API 默认在 9000 端口,控制台在 9001。容器起好后先摸一下健康检查:
bash
curl --fail http://<rustfs-host>:9000/health
建桶用 AWS CLI 最省事,--endpoint-url 指向 RustFS 即可(用法出自 RustFS 官方 AWS CLI 文档。注意一个细节:AWS CLI 对 RustFS 走的是路径风格寻址,这个信息后面配 registry 时会再用到):
bash
aws configure # 填访问密钥、私钥,region 填 us-east-1
aws s3 mb s3://docker-registry --endpoint-url http://<rustfs-host>:9000
密钥有两个来源:容器启动时通过 RUSTFS_ACCESS_KEY / RUSTFS_SECRET_KEY 环境变量设置的根密钥,或者在控制台(9001 端口)的"访问密钥"页新建,可带过期时间和描述,操作步骤见 IAM 访问密钥文档。给 registry 单独发一把密钥比复用根密钥干净,回收也方便。还有一件容易踩的事:这把密钥要通过 registry 自己的配置传进去,S3 驱动不读 AWS 凭证环境链,指望它自动继承 AWS_ACCESS_KEY_ID 那套变量,push 时会撞 InvalidAccessKeyId,RustFS 官方集成指南的排查表里专门列了这条。
两个提醒:桶必须在 registry 启动前建好,Distribution 的文档写明 bucket 要在驱动初始化之前就存在;本地试玩时 RustFS 的默认密钥是 rustfsadmin/rustfsadmin,出了本机就不该再见到这对值。
Registry 侧:一份能直接抄的 config.yml
下面这份配置的键名逐个对照过官方 S3 存储驱动文档:
yaml
version: 0.1
log:
fields:
service: registry
storage:
s3:
accesskey: <RustFS 访问密钥>
secretkey: <RustFS 私钥>
region: us-east-1
bucket: docker-registry
regionendpoint: http://<rustfs-host>:9000
forcepathstyle: true
secure: false
delete:
enabled: true
http:
addr: :5000
几处容易犹豫的键说明一下:
regionendpoint指向 S3 兼容存储的端点,用 Amazon S3 原厂时反而不要填它;forcepathstyle: true正好和上一节的路径风格对上。官方 S3 驱动文档对regionendpoint的说明原话是 from version 3+ it's required to be used withforcepathstyle: true:regionendpoint会覆盖 API 域名,从 Distribution 3 起必须显式开路径风格,只填前者驱动起不来(官方为此专门开过 issue);secure: false是因为示例里 RustFS 走的是 HTTP,两者之间上了 TLS 就改回 true(自签证书的信任问题见下一节);region对 S3 兼容存储填us-east-1即可,RustFS 官方文档的 AWS CLI 示例也用这个值;delete.enabled开着,后面垃圾回收才有东西可删。
起容器,把配置文件挂到 /etc/distribution/config.yml,这是 registry:3 镜像内配置文件的固定路径,容器内监听 5000 端口:
bash
docker run -d --name registry --restart unless-stopped \
-p 5000:5000 \
-v $PWD/config.yml:/etc/distribution/config.yml \
registry:3
版本在这里固定一下:本文按 registry:3 写,老镜像 registry:2 对应 Distribution 2.x,行为有差异,别混着抄。对照官方材料时留意一个口径:RustFS 官方集成指南是用 registry:2 验证的(所以那页的示例看不到 forcepathstyle 这个键),registry:3 的关键差异就是必须显式开它。配置写法也有两种:全部走 REGISTRY_STORAGE_S3_* 环境变量(官方指南用的就是这种),或像本文这样写 config.yml,两种写法都能用(同时存在时环境变量优先,官方不建议用环境变量覆盖整节配置),本文选后者是为了把所有键一次看全。

推一遍、拉一遍,再进桶里看一眼
bash
docker pull busybox
docker tag busybox <registry-host>:5000/team/base/busybox:latest
docker push <registry-host>:5000/team/base/busybox:latest
另找一台机器拉同一个地址,能拉下来,仓库对外就是通的。然后到 RustFS 里确认数据真的落了桶:
bash
aws s3 ls s3://docker-registry --recursive \
--endpoint-url http://<rustfs-host>:9000 | head
能看到 docker/registry/v2/ 开头的一串对象,那就是镜像的 layer 和 manifest(描述镜像由哪些层组成的清单文件)。想从 registry 侧再确认一遍,可以敲一下它的 API:
bash
curl http://<registry-host>:5000/v2/_catalog
返回的仓库列表里出现 team/base/busybox 就对上了。还有一个值得知道的细节:S3 后端下,registry 收到拉取请求时会返回 307,让客户端拿着预签名 URL(带签名的临时直链)去直连 RustFS,大 blob 的流量由 RustFS 直接吐给拉取方,registry 进程本身不扛传输。这意味着必须能连上 RustFS 的不只是 registry------每一台拉镜像的机器都得能直连 RustFS 的 S3 端点 。本文的拓扑里大家都在内网,天然满足;但如果哪天 RustFS 只在内网可达、构建机在外面,症状会很迷惑:push 正常(上传走 registry),pull 卡死(下载被重定向)。解法也有两个:S3 驱动有个 redirectendpoint 参数,专门用来生成对外可达端点的预签名 URL,适合内外网端点分离的架构;或者把 redirect.disable 设为 true,让 registry 自己代理传输,代价是放弃直连的轻量优势。这也是这套架构里 registry 本身很轻的原因。
CI 接进来:改一个地址的事
CI 侧没有黑魔法,把镜像地址从 Docker Hub 换成内网 registry 就完事。以 GitLab CI 为例:
yaml
build:
stage: build
image: docker
variables:
IMAGE: registry.internal:5000/team/app:$CI_COMMIT_SHORT_SHA
script:
- echo "$REGISTRY_PASSWORD" | docker login registry.internal:5000 -u "$REGISTRY_USER" --password-stdin
- docker build -t "$IMAGE" .
- docker push "$IMAGE"
部署端同样把 pull 地址指过来。基础镜像可以定期从 Hub 同步一份进内网仓库(skopeo 之类工具都能干;这是把镜像复制进来,不是 daemon 的 mirror 代理缓存,两码事)。同步脚本也得控制节奏:Hub 的限流按拉取计,不认你用什么客户端,大批量无脑同步,同步阶段自己就先撞上 429。同步到位后,构建环节就彻底不碰 Hub 了。还有一件容易忘的事:镜像只进不出,桶迟早被废弃 layer 撑满,得靠垃圾回收定期清,执行前提见下一节。收益按克制的方式说:配额上限从 Hub 公布的那组公共数字,变成你自己的存储容量和带宽;拉取速度取决于内网和 RustFS 的吞吐,网上那些"快了 N 倍"的说法,还是以你们自己环境的实测为准。
三个高频坑
regionendpoint 没带 forcepathstyle。 从 Distribution 3 起,regionendpoint 必须搭配 forcepathstyle: true,只填前者,registry 起不来,日志里是 S3 驱动初始化失败。
拉取端报 http: server gave HTTP response to HTTPS client。 registry 对外是 HTTP 时,每台拉取机器的 Docker daemon 都要把这个地址加进 insecure-registries 再重启 daemon。正式环境更建议直接给 registry 挂 TLS,Distribution 官方有不安全配置的说明页,也给了 nginx 反代终结 TLS 的配方。RustFS 自身也能上 TLS(RUSTFS_TLS_PATH 环境变量,见官方 TLS 配置),registry 与它之间把 secure 改回 true 即可。但改 true 之前有个前提:registry 容器必须信任 RustFS 的证书。RustFS 用自签证书时,Go 的 TLS 客户端不会买账,S3 驱动握手直接失败,日志里是 x509: certificate signed by unknown authority。解法是把自签 CA 装进 registry 容器的系统信任链(证书挂到 /usr/local/share/ca-certificates/ 后执行 update-ca-certificates),或者给容器设 AWS_CA_BUNDLE 环境变量指向证书文件。
以为镜像数据在 registry 容器里。 S3 后端下容器里几乎没有状态,删容器重建不丢数据;备份的对象是 RustFS 的数据卷和桶,registry 容器里没有需要备份的东西。清理镜像要走 registry 的垃圾回收(官方 GC 文档),且 storage.delete.enabled 得开着,直接去桶里删对象会让 registry 的元数据对不上。GC 本身也不是随手就能跑的:官方文档明确要求执行期间 registry 处于只读模式或干脆停掉:GC 边扫边有新镜像 push 进来,刚上传的 layer 还没被 manifest 引用住,可能被当成垃圾误删,产出损坏的镜像。正规做法是先让 registry 以 storage.maintenance.readonly.enabled: true(环境变量 REGISTRY_STORAGE_MAINTENANCE_READONLY_ENABLED=true)重启,跑完 GC 再切回来,所以别把 GC 裸挂成 cron 一跑了之。另外,同一支团队要是还想跑第二套 registry(比如给测试环境单开一套),可以在 S3 驱动里加一个 rootdirectory 键作为对象前缀,同一个桶里隔出互不干扰的命名空间,官方文档对它有说明。
上生产前的最后一张单子
- RustFS 健康检查
curl http://<host>:9000/health通; - 桶在 registry 启动前已建好,registry 用的是独立访问密钥,既不是根密钥也不是默认的 rustfsadmin;
- config.yml 里
regionendpoint、forcepathstyle: true、secure三个键逐一核对; - registry 镜像 tag 固定,别用 latest 漂着;
- CI 登录、push、部署机 pull 三步各跑通一次;
- 每台拉取机的 daemon 配置检查过
insecure-registries,或者 registry 已挂 TLS; - RustFS 若启用了 TLS,registry 容器已信任其证书(自签 CA 装进信任链,或设了
AWS_CA_BUNDLE); - 垃圾回收任务执行时切只读模式再跑,不是裸 cron。
相关入口:Distribution S3 存储驱动文档、Distribution GitHub 仓库、RustFS Docker Registry 集成指南、RustFS Harbor 集成指南(需要在同一桶上要 UI、RBAC 或复制能力时看这个)、RustFS Docker 安装、RustFS GitHub 仓库。