自建 Docker Hub 镜像加速:基于 Cloudflare Worker 的零成本方案

2026-07-03

在服务器上 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 — 获取认证 token
  • registry-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 环境(装 wrangler CLI 用)

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 上,也可以直接拿去做你自己的版本。