Docker Hub 从实行速率限制以来,不少团队都遇到过莫名其妙的上游 429 错误。明明代码没改,构建突然挂了;或者机器一重启,docker pull 卡半天最后抛出 toomanyrequests: You have reached your pull rate limit。这不是网络抖动,而是 Docker Hub 对匿名用户和免费账户的拉取次数做了硬性配额:匿名用户每 6 小时只能拉取 100 次(按 IP 计算),登录的免费用户每 6 小时 200 次(按账户计算)。对于有十几台服务器或者高频 CI 构建的团队来说,这个配额根本不够用。解决办法主要有三条路:买订阅、换镜像源、自建缓存代理。前两条都有外部依赖,第三条完全可控,本文重点展开。

一、先搞清楚限流到底怎么算的
很多人以为限流是按账户算的,其实匿名场景下完全按来源 IP 计算。这意味着如果你的服务器都在同一个 NAT 出口后面,比如办公室网络或者云上同一台网关,所有机器共享同一个 IP 的 100 次配额,很容易被一台机器的重启操作打光。而如果走的是公网直接拉取,云厂商的 NAT 网关 IP 段往往早已被别人消耗过配额,你一上来就已经是被限流的状态。
判断自己是否被限流,最直接的方式是查 token。Docker Registry 的拉取流程是先拿匿名 token 再请求 manifest,所以可以用下面的方式确认剩余额度:
# 匮名获取 token TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq -r .token) # 查看 RateLimit 相关响应头 curl -s -D - -o /dev/null -H "Authorization: Bearer $TOKEN" \ https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest | grep -i ratelimit
如果输出里 ratelimit-remaining 是 0,就实锤被限流了。另一个常见报错是 unauthorized: authentication required,这通常是 Docker Hub 判定你的 IP 配额耗尽,强制要求登录,配合 docker login 之后会转为按账户配额计算,能临时缓解但不能根治。
还要注意一点:每一次 docker pull 实际产生的请求数不止一次。一个镜像的拉取包含 token 获取、manifest 列表、各架构 manifest、layer 的 HEAD 探测和下载等多个请求,虽然 layer 下载部分有独立的宽松限制,但 manifest 层的请求都计入配额。多阶段构建里每个基础镜像都要走一遍流程,配额消耗速度比想象中快得多。
二、自建 registry mirror:最直接的缓存方案
官方的 registry:2 镜像自带 proxy 缓存能力,原理是把你的 registry 配置成 pull-through cache 模式。第一次拉取时它充当代理去 Docker Hub 取数据,同时把 blob 落地到本地磁盘;后续拉取直接命中本地缓存,不再消耗 Docker Hub 配额。这是投入最小、见效最快的方案。
先准备一份 registry 配置文件 config.yml:
version: 0.1
log:
fields:
service: registry
storage:
filesystem:
rootdirectory: /var/lib/registry
delete:
enabled: true
http:
addr: :5000
headers:
X-Forwarded-Proto: [https]
proxy:
remoteurl: https://registry-1.docker.io
# 建议填上付费或免费账户的凭据,缓存拉取时按账户计配额
username: yourdockeruser
password: yourdockertoken然后用 Docker Compose 启动:
services:
registry:
image: registry:2
container_name: registry-cache
restart: always
volumes:
- ./config.yml:/etc/docker/registry/config.yml
- ./data:/var/lib/registry
ports:
- "5000:5000"关键在客户端配置。编辑各台机器的 /etc/docker/daemon.json,让 Docker daemon 把 Docker Hub 的请求透明转发到缓存服务器:
{
"registry-mirrors": ["https://registry-cache.internal.ippipp.com"]
}改完执行 systemctl restart docker 生效。之后所有对 Docker Hub 官方镜像的拉取都会先走缓存,命中就不回源。注意这里的转发只对 docker.io 生效,像 quay.io、ghcr.io 这类第三方仓库不受 registry-mirrors 影响,需要单独处理。
这个方案有几个细节容易踩坑。第一,mirror 走 HTTP 的话 daemon 会拒绝,生产环境建议挂上内部证书或 Nginx 终结 TLS;第二,缓存服务器宕机时客户端会自动回退直连 Docker Hub,可用性不会被打断,但要注意回退期间的配额消耗;第三,缓存不区分租户,任何能访问 mirror 的机器都能命中缓存,这其实是优点,团队内一个人拉过的镜像,其他人秒拉。
三、加一层 Nginx:多上游容灾与缓存控制
单靠 registry mirror 只能缓存 Docker Hub,而且上游一旦抽风就没有退路。进阶做法是在 mirror 前面加一层 Nginx 反向代理,一方面终结 TLS,另一方面实现多上游切换。这样即使 Docker Hub 彻底不可用,还能切到国内可用的镜像加速地址或其他 registry。
一个实用的 Nginx 配置思路:
upstream docker_mirror {
server 127.0.0.1:5000; # 本地 registry 缓存
server mirror-2.internal:5000 backup; # 备用缓存节点
}
server {
listen 443 ssl;
server_name registry-cache.internal.ippipp.com;
ssl_certificate /etc/nginx/certs/cache.crt;
ssl_certificate_key /etc/nginx/certs/cache.key;
# Docker registry 客户端会带大请求头,适当放宽
client_max_body_size 0;
chunked_transfer_encoding on;
location / {
proxy_pass http://docker_mirror;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 900;
}
}这套架构里 Nginx 承担三件事:TLS 卸载、按路径分发到不同缓存池、上游故障时自动切换。如果想覆盖 ghcr.io、quay.io 等其他源,可以再起多个 registry 实例分别代理不同 remoteurl,然后在 Nginx 按 Host 或路径分流,客户端用重写 registry-mirrors 或改镜像地址的方式接入。
四、三种方案对比与选型建议
除了 pull-through cache,还有两种常见思路:一是全量同步工具(如 skopeo 定时同步一批常用镜像到私有仓库),二是直接依赖公共加速地址。三者各有取舍,简单对比如下:
| 方案 | 部署成本 | 缓存粒度 | 容灾能力 | 适合场景 |
|---|---|---|---|---|
| registry mirror 缓存 | 低 | 按需缓存,拉过即存 | 上游挂了可回退直连 | 中小团队日常使用 |
| skopeo 全量定时同步 | 中 | 预先定义镜像清单 | 完全不依赖上游在线 | CI 严格受控、离线环境 |
| 公共镜像加速地址 | 零 | 不可控 | 依赖第三方可用性 | 个人开发临时救急 |
给出一个 skopeo 定时同步的例子,适合把核心基础镜像固化到私有仓库:
# 从 Docker Hub 同步 nginx 多架构镜像到私有仓库 skopeo sync --src docker --dest docker \ --src-creds dockeruser:token \ --dest-creds internaluser:internalpass \ --multi-all \ docker.io/library/nginx:1.27 \ registry.internal.ippipp.com/library/nginx:1.27
配到 crontab 里每天跑一次即可。这个方案的缺点是清单要自己维护,新增镜像忘了同步就会构建失败,所以更适合和 mirror 缓存组合使用:mirror 兜住日常随机拉取,同步清单兜住核心依赖。
五、验证与日常运维
部署完不代表万事大吉,需要验证缓存真的在工作。最简单的判断方法:第一次拉取观察 registry 容器日志出现回源请求,第二次在另一台机器拉同一镜像,日志里应该只有本地 blob 写入或直接静默命中,且拉取速度明显变快。也可以直接查缓存目录:
# 查看已缓存的镜像仓库列表 curl -s http://127.0.0.1:5000/v2/_catalog | jq # 查看某个镜像的已缓存 tag curl -s http://127.0.0.1:5000/v2/library/nginx/tags/list | jq # 查看缓存磁盘占用 du -sh /var/lib/registry
运维上重点关注磁盘增长。缓存没有自动清理机制,long term 会越积越大,可以定期用 registry 的 delete API 清理无人引用的 blob,或者更省事的办法是直接用 docker exec registry-cache registry garbage-collect /etc/docker/registry/config.yml 做 GC。注意 GC 前要先删掉 manifest 才能回收对应 blob。另外记得监控缓存节点的磁盘和可用性,缓存挂掉虽然客户端能回退,但大量机器同时回源直连,配额会瞬间被打穿,这点在规模大的环境里尤其要当心。
最后提醒一句安全配置:mirror 不要裸奔在公网,任何人都能白嫖你的缓存带宽甚至枚举你的镜像列表。用防火墙限制来源 IP,或在 Nginx 层加基础认证(注意 Docker daemon 的 registry-mirrors 不支持带认证的 mirror,所以内部网段隔离是最稳妥的做法)。配置做好之后,团队内部的镜像拉取基本告别 429,构建速度也会因为本地命中而有肉眼可见的提升。
Docker Hub 限流镜像缓存registry proxy修改时间:2026-09-06 06:26:49