前言

  • 最近国内的docker镜像站已经算是全军覆没了,具体什么原因也无从知晓,在拉取镜像的时候直接显示连接超时,为了能正常拉取镜像,最好的办法就是搭建一个自己专属的代理,这里我在GitHub上找到了一个非常不错的项目,可以完美解决镜像无法拉取的情况,而且还可以搭建出一个hub镜像站,方便搜索镜像名称。
    基于 Cloudflare Workers 搭建一个专属的 Docker 镜像站以及拉取代理

什么是Cloudflare Wokers

  • Cloudflare Workers 是 Cloudflare 提供的一个服务器less(无服务器)计算服务,它允许开发者在 Cloudflare 的全球边缘网络上运行 JavaScript、Rust 或其他 WASM(WebAssembly)支持的语言编写的代码。通过这种方式,你的代码能够在离用户最近的地理位置上运行,从而实现低延迟和高性能的用户体验。
    以下是 Cloudflare Workers 的一些主要特点:
  1. 无服务器计算:
    你不需要管理或维护服务器,而是只需要关注编写和部署代码。Cloudflare 会为你处理基础设施和扩展问题。
  2. 边缘计算:
    代码直接运行在 Cloudflare 的全球边缘网络上,而不是集中在某个地区的服务器上。这意味着你的代码可以在离用户最近的地方运行,实现低延迟和高速度。
  3. 多语言支持:
    虽然最初是为 JavaScript 设计的,但现在 Cloudflare Workers 也支持 Rust 和任何能编译成 WebAssembly 的语言。
  4. 简单的部署和管理:
    Cloudflare 提供了简单的命令行工具和管理界面,使得部署和管理你的 Workers 变得非常简单。
  5. 内置的键值存储:
    Cloudflare Workers 附带了一个名为 Workers KV 的内置键值存储解决方案,使得你可以在边缘网络上存储和检索数据。
  6. HTTP 路由和请求处理:
    你可以轻松地创建 HTTP 路由,处理 HTTP 请求和响应,以及修改传入和传出的 HTTP 流量。
  7. 安全性和隐私:
    Cloudflare Workers 运行在一个安全的沙箱环境中,以保护你的代码和数据。
  8. 集成和生态系统:
    Cloudflare Workers 可以与 Cloudflare 的其他产品和服务集成,如 Cloudflare Pages、Durable Objects 和 Cloudflare Access 等。
    Cloudflare Workers 为开发者提供了一个灵活、高性能、并且易于使用的边缘计算平台,使得你可以构建和部署全球分布式应用。

准备工作

部署方式

  • 打开Cloudflare的控制台,点Workers 和 Pages,在点击创建-创建Worker,这里可以自定义想要的名称,也可以使用默认的,我就输了个docker-proxy,点击创建,然后直接点编辑代码
  • 打开项目地址:https://github.com/cmliu/CF-Workers-docker.io/blob/main/_worker.js
    直接复制里面的代码,这里有些人可能打不开这个地址,这里也把代码直接放在下面了,点击小箭头可以打开查看,也可以直接点击复制
  1. // _worker.js
  2. // Docker镜像仓库主机地址
  3. let hub_host = 'registry-1.docker.io'
  4. // Docker认证服务器地址
  5. const auth_url = 'https://auth.docker.io'
  6. // 自定义的工作服务器地址
  7. let workers_url = 'https://你的域名地址比如 docker.mydomain.com'
  8. let 屏蔽爬虫UA = ['netcraft'];
  9. // 根据主机名选择对应的上游地址
  10. function routeByHosts(host) {
  11. // 定义路由表
  12. const routes = {
  13. // 生产环境
  14. "quay": "quay.io",
  15. "gcr": "gcr.io",
  16. "k8s-gcr": "k8s.gcr.io",
  17. "k8s": "registry.k8s.io",
  18. "ghcr": "ghcr.io",
  19. "cloudsmith": "docker.cloudsmith.io",
  20. // 测试环境
  21. "test": "registry-1.docker.io",
  22. };
  23. if (host in routes) return [ routes[host], false ];
  24. else return [ hub_host, true ];
  25. }
  26. /** @type {RequestInit} */
  27. const PREFLIGHT_INIT = {
  28. // 预检请求配置
  29. headers: new Headers({
  30. 'access-control-allow-origin': '*', // 允许所有来源
  31. 'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS', // 允许的HTTP方法
  32. 'access-control-max-age': '1728000', // 预检请求的缓存时间
  33. }),
  34. }
  35. /**
  36. * 构造响应
  37. * @param {any} body 响应体
  38. * @param {number} status 响应状态码
  39. * @param {Object<string, string>} headers 响应头
  40. */
  41. function makeRes(body, status = 200, headers = {}) {
  42. headers['access-control-allow-origin'] = '*' // 允许所有来源
  43. return new Response(body, { status, headers }) // 返回新构造的响应
  44. }
  45. /**
  46. * 构造新的URL对象
  47. * @param {string} urlStr URL字符串
  48. */
  49. function newUrl(urlStr) {
  50. try {
  51. return new URL(urlStr) // 尝试构造新的URL对象
  52. } catch (err) {
  53. return null // 构造失败返回null
  54. }
  55. }
  56. function isUUID(uuid) {
  57. // 定义一个正则表达式来匹配 UUID 格式
  58. const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
  59. // 使用正则表达式测试 UUID 字符串
  60. return uuidRegex.test(uuid);
  61. }
  62. async function nginx() {
  63. const text = `
  64. <!DOCTYPE html>
  65. <html>
  66. <head>
  67. <title>Welcome to nginx!</title>
  68. <style>
  69. body {
  70. width: 35em;
  71. margin: 0 auto;
  72. font-family: Tahoma, Verdana, Arial, sans-serif;
  73. }
  74. </style>
  75. </head>
  76. <body>
  77. <h1>Welcome to nginx!</h1>
  78. <p>If you see this page, the nginx web server is successfully installed and
  79. working. Further configuration is required.</p>
  80. <p>For online documentation and support please refer to
  81. <a href="http://nginx.org/">nginx.org</a>.<br/>
  82. Commercial support is available at
  83. <a href="http://nginx.com/">nginx.com</a>.</p>
  84. <p><em>Thank you for using nginx.</em></p>
  85. </body>
  86. </html>
  87. `
  88. return text ;
  89. }
  90. export default {
  91. async fetch(request, env, ctx) {
  92. const getReqHeader = (key) => request.headers.get(key); // 获取请求头
  93. let url = new URL(request.url); // 解析请求URL
  94. const userAgentHeader = request.headers.get('User-Agent');
  95. const userAgent = userAgentHeader ? userAgentHeader.toLowerCase() : "null";
  96. if (env.UA) 屏蔽爬虫UA = 屏蔽爬虫UA.concat(await ADD(env.UA));
  97. workers_url = `https://${url.hostname}`;
  98. const pathname = url.pathname;
  99. const hostname = url.searchParams.get('hubhost') || url.hostname;
  100. const hostTop = hostname.split('.')[0];// 获取主机名的第一部分
  101. const checkHost = routeByHosts(hostTop);
  102. hub_host = checkHost[0]; // 获取上游地址
  103. const fakePage = checkHost[1];
  104. console.log(`域名头部: ${hostTop}\n反代地址: ${hub_host}\n伪装首页: ${fakePage}`);
  105. const isUuid = isUUID(pathname.split('/')[1].split('/')[0]);
  106. if (屏蔽爬虫UA.some(fxxk => userAgent.includes(fxxk)) && 屏蔽爬虫UA.length > 0){
  107. //首页改成一个nginx伪装页
  108. return new Response(await nginx(), {
  109. headers: {
  110. 'Content-Type': 'text/html; charset=UTF-8',
  111. },
  112. });
  113. }
  114. const conditions = [
  115. isUuid,
  116. pathname.includes('/_'),
  117. pathname.includes('/r'),
  118. pathname.includes('/v2/user'),
  119. pathname.includes('/v2/orgs'),
  120. pathname.includes('/v2/_catalog'),
  121. pathname.includes('/v2/categories'),
  122. pathname.includes('/v2/feature-flags'),
  123. pathname.includes('search'),
  124. pathname.includes('source'),
  125. pathname === '/',
  126. pathname === '/favicon.ico',
  127. pathname === '/auth/profile',
  128. ];
  129. if (conditions.some(condition => condition) && (fakePage === true || hostTop == 'docker')) {
  130. if (env.URL302){
  131. return Response.redirect(env.URL302, 302);
  132. } else if (env.URL){
  133. if (env.URL.toLowerCase() == 'nginx'){
  134. //首页改成一个nginx伪装页
  135. return new Response(await nginx(), {
  136. headers: {
  137. 'Content-Type': 'text/html; charset=UTF-8',
  138. },
  139. });
  140. } else return fetch(new Request(env.URL, request));
  141. }
  142. const newUrl = new URL("https://registry.hub.docker.com" + pathname + url.search);
  143. // 复制原始请求的标头
  144. const headers = new Headers(request.headers);
  145. // 确保 Host 头部被替换为 hub.docker.com
  146. headers.set('Host', 'registry.hub.docker.com');
  147. const newRequest = new Request(newUrl, {
  148. method: request.method,
  149. headers: headers,
  150. body: request.method !== 'GET' && request.method !== 'HEAD' ? await request.blob() : null,
  151. redirect: 'follow'
  152. });
  153. return fetch(newRequest);
  154. }
  155. // 修改包含 %2F 和 %3A 的请求
  156. if (!/%2F/.test(url.search) && /%3A/.test(url.toString())) {
  157. let modifiedUrl = url.toString().replace(/%3A(?=.*?&)/, '%3Alibrary%2F');
  158. url = new URL(modifiedUrl);
  159. console.log(`handle_url: ${url}`)
  160. }
  161. // 处理token请求
  162. if (url.pathname.includes('/token')) {
  163. let token_parameter = {
  164. headers: {
  165. 'Host': 'auth.docker.io',
  166. 'User-Agent': getReqHeader("User-Agent"),
  167. 'Accept': getReqHeader("Accept"),
  168. 'Accept-Language': getReqHeader("Accept-Language"),
  169. 'Accept-Encoding': getReqHeader("Accept-Encoding"),
  170. 'Connection': 'keep-alive',
  171. 'Cache-Control': 'max-age=0'
  172. }
  173. };
  174. let token_url = auth_url + url.pathname + url.search
  175. return fetch(new Request(token_url, request), token_parameter)
  176. }
  177. // 修改 /v2/ 请求路径
  178. if (/^\/v2\/[^/]+\/[^/]+\/[^/]+$/.test(url.pathname) && !/^\/v2\/library/.test(url.pathname)) {
  179. url.pathname = url.pathname.replace(/\/v2\//, '/v2/library/');
  180. console.log(`modified_url: ${url.pathname}`)
  181. }
  182. // 更改请求的主机名
  183. url.hostname = hub_host;
  184. // 构造请求参数
  185. let parameter = {
  186. headers: {
  187. 'Host': hub_host,
  188. 'User-Agent': getReqHeader("User-Agent"),
  189. 'Accept': getReqHeader("Accept"),
  190. 'Accept-Language': getReqHeader("Accept-Language"),
  191. 'Accept-Encoding': getReqHeader("Accept-Encoding"),
  192. 'Connection': 'keep-alive',
  193. 'Cache-Control': 'max-age=0'
  194. },
  195. cacheTtl: 3600 // 缓存时间
  196. };
  197. // 添加Authorization头
  198. if (request.headers.has("Authorization")) {
  199. parameter.headers.Authorization = getReqHeader("Authorization");
  200. }
  201. // 发起请求并处理响应
  202. let original_response = await fetch(new Request(url, request), parameter)
  203. let original_response_clone = original_response.clone();
  204. let original_text = original_response_clone.body;
  205. let response_headers = original_response.headers;
  206. let new_response_headers = new Headers(response_headers);
  207. let status = original_response.status;
  208. // 修改 Www-Authenticate 头
  209. if (new_response_headers.get("Www-Authenticate")) {
  210. let auth = new_response_headers.get("Www-Authenticate");
  211. let re = new RegExp(auth_url, 'g');
  212. new_response_headers.set("Www-Authenticate", response_headers.get("Www-Authenticate").replace(re, workers_url));
  213. }
  214. // 处理重定向
  215. if (new_response_headers.get("Location")) {
  216. return httpHandler(request, new_response_headers.get("Location"))
  217. }
  218. // 返回修改后的响应
  219. let response = new Response(original_text, {
  220. status,
  221. headers: new_response_headers
  222. })
  223. return response;
  224. }
  225. };
  226. /**
  227. * 处理HTTP请求
  228. * @param {Request} req 请求对象
  229. * @param {string} pathname 请求路径
  230. */
  231. function httpHandler(req, pathname) {
  232. const reqHdrRaw = req.headers
  233. // 处理预检请求
  234. if (req.method === 'OPTIONS' &&
  235. reqHdrRaw.has('access-control-request-headers')
  236. ) {
  237. return new Response(null, PREFLIGHT_INIT)
  238. }
  239. let rawLen = ''
  240. const reqHdrNew = new Headers(reqHdrRaw)
  241. const refer = reqHdrNew.get('referer')
  242. let urlStr = pathname
  243. const urlObj = newUrl(urlStr)
  244. /** @type {RequestInit} */
  245. const reqInit = {
  246. method: req.method,
  247. headers: reqHdrNew,
  248. redirect: 'follow',
  249. body: req.body
  250. }
  251. return proxy(urlObj, reqInit, rawLen)
  252. }
  253. /**
  254. * 代理请求
  255. * @param {URL} urlObj URL对象
  256. * @param {RequestInit} reqInit 请求初始化对象
  257. * @param {string} rawLen 原始长度
  258. */
  259. async function proxy(urlObj, reqInit, rawLen) {
  260. const res = await fetch(urlObj.href, reqInit)
  261. const resHdrOld = res.headers
  262. const resHdrNew = new Headers(resHdrOld)
  263. // 验证长度
  264. if (rawLen) {
  265. const newLen = resHdrOld.get('content-length') || ''
  266. const badLen = (rawLen !== newLen)
  267. if (badLen) {
  268. return makeRes(res.body, 400, {
  269. '--error': `bad len: ${newLen}, except: ${rawLen}`,
  270. 'access-control-expose-headers': '--error',
  271. })
  272. }
  273. }
  274. const status = res.status
  275. resHdrNew.set('access-control-expose-headers', '*')
  276. resHdrNew.set('access-control-allow-origin', '*')
  277. resHdrNew.set('Cache-Control', 'max-age=1500')
  278. // 删除不必要的头
  279. resHdrNew.delete('content-security-policy')
  280. resHdrNew.delete('content-security-policy-report-only')
  281. resHdrNew.delete('clear-site-data')
  282. return new Response(res.body, {
  283. status,
  284. headers: resHdrNew
  285. })
  286. }
  287. async function ADD(envadd) {
  288. var addtext = envadd.replace(/[ |"'\r\n]+/g, ',').replace(/,+/g, ','); // 将空格、双引号、单引号和换行符替换为逗号
  289. //console.log(addtext);
  290. if (addtext.charAt(0) == ',') addtext = addtext.slice(1);
  291. if (addtext.charAt(addtext.length -1) == ',') addtext = addtext.slice(0, addtext.length - 1);
  292. const add = addtext.split(',');
  293. //console.log(add);
  294. return add ;
  295. }
  • 直接把原来的代码全选删除,把上面的代码复制进去,按照注释修改自定义服务器地址之后点击部署,待显示版本已保存的时候,点击左上角的箭头,在点击设置-触发器,把刚刚的自定义服务器地址添加到自定义域中,在添加一个路由地址,按照这个格式填即可:docker.mydomain.com/*
  • 稍等一会,在浏览器打开刚刚的地址试一下,不出意外的话,dockerhub将会正常打开,这表示部署已经完成
    基于 Cloudflare Workers 搭建一个专属的 Docker 镜像站以及拉取代理

使用说明

例如您的Workers项目域名为:docker.fxxk.dedyn.io

1.官方镜像路径前面加域名

  1. docker pull docker.fxxk.dedyn.io/stilleshan/frpc:latest
  1. docker pull docker.fxxk.dedyn.io/library/nginx:stable-alpine3.19-perl

2.一键设置镜像加速

修改文件 /etc/docker/daemon.json(如果不存在则创建)

  1. sudo mkdir -p /etc/docker
  2. sudo tee /etc/docker/daemon.json <<-'EOF'
  3. {
  4. "registry-mirrors": ["https://请替换为您自己的Worker自定义域名"]
  5. }
  6. EOF
  7. sudo systemctl daemon-reload
  8. sudo systemctl restart docker
  • 接下来实际测试一下,按照刚刚的方法设置一下镜像加速,再次拉取之前超时的镜像,这次就很顺利的拉下来了
    基于 Cloudflare Workers 搭建一个专属的 Docker 镜像站以及拉取代理