当前位置:首页 > 技术 > 正文内容

基于 Cloudflare Workers 构建 Docker Registry 代理服务

访客 技术 2026年9月23日 12

边缘代理架构设计

在容器化工作流中,直接访问公共镜像仓库常受限于区域网络策略,导致拉取速率低下或连接中断。通过部署边缘计算节点作为中转层,可有效解决此类问题。Cloudflare Workers 提供基于 V8 引擎的无服务器运行环境,具备天然的全球分布式特性,非常适合作为轻量级容器镜像代理网关。

该架构的核心优势在于:

  • 请求就近路由:客户端请求自动指向地理距离最近的边缘节点,大幅降低网络延迟。
  • 免运维基础设施:无需预配虚拟机或容器集群,代码随请求触发执行,支持自动弹性扩缩容。
  • 按需计量计费:费用完全基于实际处理的请求数量与执行时长,适合流量波动较大的镜像分发场景。

部署 Worker 实例

登录 Cloudflare 控制台,进入 Workers & Pages 模块,创建新的 Worker 服务。分配具有语义的名称(例如 docker-mirror),完成初始部署后跳转至在线代码编辑器。

核心代理逻辑实现

将以下 JavaScript 代码替换至编辑器中。该脚本实现了上游路由分发、鉴权协商、请求头重写及静态资源拦截等核心功能。

// 定义上游注册表与认证端点
const DEFAULT_REGISTRY_HOST = 'registry-1.docker.io';
const AUTH_SERVER_URL = 'https://auth.docker.io';
let WORKER_BASE_URL = '';

// 路由映射配置:依据子域前缀选择目标上游
const UPSTREAM_MAP = {
  'quay': 'quay.io',
  'gcr': 'gcr.io',
  'k8s-gcr': 'k8s.gcr.io',
  'k8s': 'registry.k8s.io',
  'ghcr': 'ghcr.io',
  'cloudsmith': 'docker.cloudsmith.io',
  'test': 'registry-1.docker.io'
};

function resolveUpstream(targetHost) {
  const prefix = targetHost.split('.')[0];
  const mappedHost = UPSTREAM_MAP[prefix] || DEFAULT_REGISTRY_HOST;
  const isDefaultRoute = !UPSTREAM_MAP[prefix];
  return { host: mappedHost, isDefault: isDefaultRoute };
}

// 响应构建辅助函数
function buildResponse(content, statusCode = 200, extraHeaders = {}) {
  const hdrs = new Headers(extraHeaders);
  hdrs.set('Access-Control-Allow-Origin', '*');
  return new Response(content, { status: statusCode, headers: hdrs });
}

// 校验 UUID 格式
function checkUuidFormat(str) {
  return /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(str);
}

// 生成占位 HTML
function generatePlaceholderHtml() {
  return '<!DOCTYPE html><html><head><title>Mirror Service</title></head><body><h1>Proxy Active</h1></body></html>';
}

export default {
  async fetch(request, env, ctx) {
    const requestUrl = new URL(request.url);
    const clientAgent = (request.headers.get('User-Agent') || '').toLowerCase();
    const currentDomain = requestUrl.hostname;
    WORKER_BASE_URL = `https://${currentDomain}`;

    // 动态合并环境变量中的拦截列表
    let activeBlockedAgents = ['netcraft'];
    if (env.UA_FILTER) {
      const extra = env.UA_FILTER.split(',').map(s => s.trim()).filter(Boolean);
      activeBlockedAgents = [...activeBlockedAgents, ...extra];
    }

    const routeConfig = resolveUpstream(currentDomain);
    const targetRegistry = routeConfig.host;

    // 拦截指定 UA 请求
    if (activeBlockedAgents.some(agent => clientAgent.includes(agent))) {
      return buildResponse(generatePlaceholderHtml(), 200, { 'Content-Type': 'text/html; charset=utf-8' });
    }

    const reqPath = requestUrl.pathname;
    const isSpecialPath = [
      reqPath === '/', reqPath === '/favicon.ico',
      reqPath.includes('/_catalog'), reqPath.includes('/v2/_'),
      reqPath.includes('/v2/user'), reqPath.includes('/v2/orgs'),
      reqPath.includes('/search'), reqPath.includes('/source')
    ].some(Boolean);

    // 处理根路径、特殊 API 或 UUID 请求
    if ((isSpecialPath || checkUuidFormat(reqPath.split('/')[1] || '')) && (routeConfig.isDefault || currentDomain.startsWith('docker'))) {
      if (env.REDIRECT_TARGET) {
        return Response.redirect(env.REDIRECT_TARGET, 302);
      }
      if (env.FALLBACK_URL) {
        if (env.FALLBACK_URL.toLowerCase() === 'nginx') {
          return buildResponse(generatePlaceholderHtml(), 200, { 'Content-Type': 'text/html; charset=utf-8' });
        }
        return fetch(new Request(env.FALLBACK_URL, request));
      }
      // 转发至官方 Hub 首页
      const hubRequest = new Request(`https://registry.hub.docker.com${reqPath}${requestUrl.search}`, {
        method: request.method,
        headers: new Headers({ ...Object.fromEntries(request.headers), 'Host': 'registry.hub.docker.com' }),
        body: ['GET', 'HEAD'].includes(request.method) ? null : await request.blob(),
        redirect: 'follow'
      });
      return fetch(hubRequest);
    }

    // 处理 URL 编码规范化 (%3A -> %3Alibrary/)
    let normalizedUrl = new URL(requestUrl.toString());
    if (normalizedUrl.search.includes('%3A') && !normalizedUrl.search.includes('%2F')) {
      normalizedUrl.search = normalizedUrl.search.replace(/%3A(?=.*?&)/, '%3Alibrary%2F');
    }

    // 代理 OAuth Token 认证请求
    if (normalizedUrl.pathname.includes('/token')) {
      const authRequest = new Request(`${AUTH_SERVER_URL}${normalizedUrl.pathname}${normalizedUrl.search}`, request);
      authRequest.headers.set('Host', AUTH_SERVER_URL.replace('https://', ''));
      return fetch(authRequest);
    }

    // 自动补全 Docker Hub 默认命名空间
    if (/^\/v2\/[^/]+\/[^/]+\/[^/]+$/.test(normalizedUrl.pathname) && !normalizedUrl.pathname.startsWith('/v2/library')) {
      normalizedUrl.pathname = normalizedUrl.pathname.replace(/^\/v2\//, '/v2/library/');
    }

    // 切换目标主机名
    normalizedUrl.hostname = targetRegistry;

    // 构建代理请求头
    const proxyHeaders = new Headers(request.headers);
    proxyHeaders.set('Host', targetRegistry);
    proxyHeaders.set('Connection', 'keep-alive');
    proxyHeaders.set('Cache-Control', 'max-age=0');

    const proxyRequest = new Request(normalizedUrl, {
      method: request.method,
      headers: proxyHeaders,
      body: ['GET', 'HEAD'].includes(request.method) ? null : await request.blob(),
      redirect: 'follow'
    });

    // 执行上游请求
    const upstreamResponse = await fetch(proxyRequest);
    const responseHeaders = new Headers(upstreamResponse.headers);

    // 重写认证服务器地址指向 Worker
    const wwwAuth = responseHeaders.get('Www-Authenticate');
    if (wwwAuth) {
      responseHeaders.set('Www-Authenticate', wwwAuth.replace(/https:\/\/auth\.docker\.io/g, WORKER_BASE_URL));
    }

    // 处理重定向响应
    const redirectLocation = responseHeaders.get('Location');
    if (redirectLocation) {
      const redirectReq = new Request(redirectLocation, request);
      return fetch(redirectReq);
    }

    // 注入标准响应头
    responseHeaders.set('Access-Control-Allow-Origin', '*');
    responseHeaders.set('Access-Control-Expose-Headers', '*');
    responseHeaders.set('Cache-Control', 'max-age=1500');
    responseHeaders.delete('Content-Security-Policy');
    responseHeaders.delete('Clear-Site-Data');

    return new Response(upstreamResponse.body, { status: upstreamResponse.status, headers: responseHeaders });
  }
};

粘贴代码后点击【部署】,服务即刻生效。

域名绑定与证书配置

返回 Worker 控制台,进入"设置"标签页下的"触发器"区域。点击"添加自定义域",输入预设的域名(例如 docker.yourdomain.com)。Cloudflare 会自动完成 DNS 解析验证并签发 SSL 证书,无需额外配置。

容器客户端适配

在宿主机中编辑 Docker 守护进程配置文件 /etc/docker/daemon.json,将代理地址注入镜像源列表:

{
  "registry-mirrors": ["https://docker.yourdomain.com"],
  "insecure-registries": ["docker.yourdomain.com"]
}

重载系统配置并重启 Docker 服务以应用变更:

sudo systemctl daemon-reload
sudo systemctl restart docker

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

linux screen 用法详情 (nohup 的替代方案)

一、screen 是什么?能干嘛?screen 是一个终端复用器,可以:在一个 SSH 会话中开多个“虚拟终端”SSH 断线后,程序仍然在后台运行随时重新连接到原来的会话特别适合:nohup 的替代方案跑脚本 / 爬虫 / 训练模型运维、远程开发二、安装 screen# CentOS / Rocky / Almayum install -y screen# Debian / Ubuntuapt i...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。