用 Cloudflare Worker + R2 搭 Docker Hub 镜像加速站
VPC 部署博客,Docker 镜像拉不下来,EOF 报错一堆。折腾了一圈,最后用 Cloudflare Worker + R2 搞了个自己的镜像加速站。
出了什么事
博客的部署流程是 GitHub Actions 构建 Docker 镜像,推到 Docker Hub,然后 VPC 通过 SSH 执行 deploy.sh 拉取镜像并重启容器。
之前一直好好的,最近突然开始报 EOF:
Error response from daemon: Head "https://registry-1.docker.io/v2/...": EOF
三次重试全挂,部署直接失败。
VPC 在国内,访问 Docker Hub 本来就不太稳定,这次是彻底不通了。
先试了几个公共镜像站
DaoCloud (docker.m.daocloud.io):白名单机制,只加速知名镜像。我的博客镜像 nnnnzs/react-nnnnzs-cn 不在白名单里,直接拒绝。
iyuu (hub.iyuu.cn):DNS 解析失败,站点可能已经挂了。
fnnas (docker.fnnas.com):飞牛的镜像加速站,居然能拉到。配置到 VPC 的 daemon.json 里,docker pull 走 fnnas,部署恢复了。
到这里博客部署的问题已经解决了。但我还想搞清楚一件事:我自己之前在 Cloudflare Workers 上部署过一个 Docker Hub 代理(docker-proxy.nnnnzs.cn),为什么它也报限流?
CF Worker 为什么被限流
看了下 Worker 代码,就是一个纯代理——每次请求都实时转发到 Docker Hub。问题在于 Cloudflare 的出口 IP 是共享的,Docker Hub 按 IP 段算限流配额(匿名用户 100 次/6 小时),CF 的 IP 被大量用户 share,配额很快就超了。
给 Worker 加 Docker Hub 认证
第一步是给 Worker 加上 Docker Hub 的 Personal Access Token。认证用户配额提升到 200 次/6 小时。
在 CF Workers 的 Secrets 里配了 DOCKERHUB_USERNAME 和 DOCKERHUB_PAT,Worker 在请求 Docker Hub 的 token 端点时带上 Basic Auth。
直接 curl 测试,token 端点返回的 x-ratelimit-limit: 3000,认证确实生效了。
但在 VPC 上 docker pull 还是报限流。
原因:Docker 的 mirror 协议设计如此——mirror 只代理 registry 请求,不代理 auth 请求。Docker client 用 mirror 拉镜像时,认证还是直接走 Docker Hub,不经过 Worker 的 /token 端点。所以 Worker 的认证加了等于没加。
上 R2 缓存层
既然认证走不通,那就做缓存。思路很简单:manifest 和 blob 数据存到 Cloudflare R2,大部分请求直接从 R2 返回,不打 Docker Hub。
Manifest(镜像元数据):tag 引用缓存 10 分钟,digest 引用缓存 24 小时。
Blob(layer 文件,镜像本体):永久缓存,内容寻址,同一个文件不会重复存储。
第一次拉取回源 Docker Hub,之后直接走 R2。R2 是 Cloudflare 自己的对象存储,不收出流量费,免费额度 10GB。
Worker 代码从 Service Worker 格式迁移到了 ES Module(R2 绑定需要),加了缓存逻辑。大文件(>4.5GB)跳过缓存,客户端拿到响应后异步写 R2,不阻塞下载。
VPC 上测试 docker pull docker-proxy.nnnnzs.cn/library/nginx:alpine,第二次请求返回 X-Cache: HIT。
VPC 镜像配置
最终 VPC 的 daemon.json:
json
{
"registry-mirrors": [
"https://docker-proxy.nnnnzs.cn",
"https://docker.fnnas.com",
"https://docker.m.daocloud.io"
]
}
自己 Worker 优先,fnnas 做 fallback,DaoCloud 兜底。
R2 清理
R2 的 blob 是永久存储的,不清理会慢慢涨。加了个生命周期规则:
bash
wrangler r2 bucket lifecycle add docker-hub-cache --name expire-old-blobs --prefix blobs/ --expire-days 30
超过 30 天没访问的 blob 自动删除。manifest 本身有短 TTL,不需要额外处理。
代码仓库
Worker 代码在 GitHub 私有仓库 NNNNzs/docker-proxy-worker,用 wrangler 部署。
整个方案的依赖:Cloudflare Workers(免费)+ R2(免费额度内)+ Docker Hub PAT(免费)。没有额外成本。