在服务器上 docker pull nginx:latest,等了半分钟,最后看到的是 TLS handshake timeout。这个场景国内用 Docker 的人应该都不陌生。
registry-1.docker.io 在国内的网络环境不太稳定。改 DNS、挂代理、用公共镜像站——这些方法各有各的局限性。改 DNS 效果有限,挂代理要额外配置,公共镜像站速度不稳定、有些还逐步停用了。
本文介绍另一种思路:用 Cloudflare Workers 自建一个 Docker Hub 反代,免费、零运维,5 分钟部署完。
方案定位

这个方案依赖一个前提条件:你有一个域名托管在 Cloudflare。如果已经有了,那几乎零成本——Workers 免费版每天 10 万次请求,个人和小团队完全够用。
和几个常见方案对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 改 DNS(如 114.114.114.114) | 零配置 | 效果有限,IPv6 问题多 |
| 挂代理客户端 | 全局生效 | 需要额外软件,部分环境不适用 |
| 用公共镜像站 | 零配置 | 不稳定,部分已停用 |
| 自建 nginx 反代 | 可控 | 需要云主机,有运维成本 |
| Cloudflare Workers 反代(本文) | 免费、零运维、5 分钟 | 需要 Cloudflare 托管的域名 |
原理

Docker 拉镜像时主要访问两个域名:
auth.docker.io— 获取认证 tokenregistry-1.docker.io— 获取 manifest 和镜像层(blob)
Worker 部署在你自己的域名下,根据请求路径自动分流到对应的官方域名。请求流变成:
docker pull nginx
→ 你的域名(如 docker.yourdomain.com)
→ Worker 判断路径
→ /token → 转发给 auth.docker.io
→ /v2/ → 转发给 registry-1.docker.io
→ 返回结果(改写关键头)
有三个必须处理的细节,后面踩坑部分会展开说。
部署步骤
前置条件
- 一个域名托管在 Cloudflare(必须)
- Node.js 环境(装
wranglerCLI 用)
1. 初始化项目
mkdir docker-hub-proxy && cd docker-hub-proxy
npm init -y
npm install wrangler --save-dev
2. 编写 Worker 核心代码
创建 src/index.ts:
// 路径分流
const DOCKER_HUB = 'registry-1.docker.io';
const AUTH_SERVER = 'auth.docker.io';
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// auth token 请求走 auth.docker.io
if (url.pathname === '/token' || url.pathname.startsWith('/v2/token')) {
return proxyRequest(request, `https://${AUTH_SERVER}${url.pathname}${url.search}`);
}
// 其他走 registry-1.docker.io
return proxyRequest(request, `https://${DOCKER_HUB}${url.pathname}${url.search}`);
}
};
async function proxyRequest(request: Request, targetUrl: string): Promise<Response> {
// 构建新请求,清理 CF 内部头
const headers = new Headers(request.headers);
headers.delete('cf-connecting-ip');
headers.delete('cf-ray');
headers.delete('cf-worker');
const modifiedRequest = new Request(targetUrl, {
method: request.method,
headers,
body: request.method !== 'GET' && request.method !== 'HEAD' ? request.body : null,
redirect: 'manual', // 关键:不要自动跟随重定向
});
const response = await fetch(modifiedRequest);
// 只保留关键响应头
const allowedHeaders = [
'content-type', 'content-length', 'docker-content-digest',
'docker-distribution-api-version', 'etag', 'link',
'ratelimit-remaining', 'ratelimit-limit',
'www-authenticate', 'location', 'cache-control',
];
const responseHeaders = new Headers();
for (const key of allowedHeaders) {
const value = response.headers.get(key);
if (value) responseHeaders.set(key, value);
}
// 重写 Www-Authenticate 头中的 realm
const authHeader = responseHeaders.get('www-authenticate');
if (authHeader) {
const rewritten = authHeader.replace(
/realm="https?:\/\/[^"]+"/,
`realm="${new URL(request.url).origin}/token"`
);
responseHeaders.set('www-authenticate', rewritten);
}
// CORS(便于调试,docker pull 本身不需要)
responseHeaders.set('access-control-allow-origin', '*');
return new Response(response.body, {
status: response.status,
headers: responseHeaders,
});
}
3. 配置 wrangler.toml
name = "docker-hub-proxy"
main = "src/index.ts"
compatibility_flags = ["nodejs_compat"]
# 关闭 workers.dev 域名(国内不可用)
workers_dev = false
# 绑定自定义域名
routes = [
{ pattern = "docker.yourdomain.com", custom_domain = true }
]
4. 部署
npx wrangler login # 浏览器授权
npx wrangler deploy
出现 https://docker.yourdomain.com 即成功。
5. 验证
# 检查 docker API 是否正常
curl -I https://docker.yourdomain.com/v2/
# 应该返回 401,且 Www-Authenticate 头中的 realm 指向你的域名
# 测试 token 获取
curl "https://docker.yourdomain.com/token?service=registry.docker.io&scope=repository:library/nginx:pull"
# 测试 manifest 拉取
curl -H "Accept: application/vnd.docker.distribution.manifest.list.v2+json" \
https://docker.yourdomain.com/v2/library/nginx/manifests/latest
使用方式
方式 A:镜像加速器(推荐)
编辑 /etc/docker/daemon.json:
{
"registry-mirrors": ["https://docker.yourdomain.com"]
}
重启 Docker:
sudo systemctl daemon-reload && sudo systemctl restart docker
之后 docker pull nginx:latest 完全不用改命令,走你的加速器。
方式 B:前缀式拉取
适合不想改 daemon.json 的场景,比如只在某台机器上用:
docker pull docker.yourdomain.com/library/nginx:latest
docker pull docker.yourdomain.com/bitnami/redis:latest
公有和私有镜像都适用。
私有镜像
docker login docker.yourdomain.com
# Username: Docker Hub 用户名
# Password: 推荐用 Access Token
docker pull docker.yourdomain.com/yourname/private-app:latest
踩坑记录
1. Www-Authenticate 头重写
Docker 客户端拉镜像时,auth.docker.io 返回的 Www-Authenticate 头里的 realm 字段指向 auth.docker.io/token。如果你的域名代理了这个请求但没有改写这个字段,Docker 客户端会绕过你的代理直接去请求 auth.docker.io,代理就失效了。
解决方案:拦截 www-authenticate 响应头,把 realm 替换成你的域名。
2. redirect 必须 manual
Worker 默认 redirect: 'follow' 会跟随重定向到 Docker Hub 官方域名,然后你的代理就脱链了,后面的请求不再经过你。用 redirect: 'manual' 手动处理,然后把 location 头原样返回,让 Docker 客户端来跟随。
3. workers.dev 域名国内不可用
Cloudflare 提供的 *.workers.dev 默认域名在国内被 DNS 污染和 SNI 阻断,必须绑自定义域名。wrangler.toml 里设 workers_dev = false,然后通过 routes 绑定你的子域名。
常见问题
免费额度够用吗?
Workers 免费版每天 10 万次请求。一次 docker pull 通常产生几十个请求(manifest + 各层 blob),个人和小团队够用。
缓存会泄露私有镜像吗?
缓存命中时,Worker 会拿调用方的 Authorization 向 Docker Hub 发一次 HEAD 请求做鉴权。没权限就走回源,不会把别人的私有镜像层返回给你。
支持 ghcr.io / gcr.io 吗?
本文方案只反代 Docker Hub。需要其他 registry 的话,在 Worker 里加对应的路由分支即可,原理是一样的。
总结
这是一个门槛低、见效快的方案。如果你本来就有域名在 Cloudflare,5 分钟能搭完。完整的代码(约 140 行)在 GitHub 上,也可以直接拿去做你自己的版本。