MeArchive#MeArchive#剪藏

自建Docker Hub加速镜像

2026 年 4 月 14 日1 分钟
分享Twitter / XTelegram微博

同步来源:MeArchive 源站剪藏 ID:11 原文地址:https://blog.lty520.faith/%E5%8D%9A%E6%96%87/%E8%87%AA%E5%BB%BAdocker-hub%E5%8A%A0%E9%80%9F%E9%95%9C%E5%83%8F/#%e6%96%b9%e6%a1%88%e4%b8%80%e4%ba%8c%e6%95%b4%e5%90%88

打开原文 · 打开源站剪藏快照


自建Docker Hub加速镜像

自建Docker Hub加速镜像

在使用Docker的过程中会大量的涉及Pull镜像的操作,但是由于官方的镜像服务器在国外再加上某个防火墙的阻拦,导致直接拉取镜像非常困难(看脸)。所以通常的操作是设置一个由国内厂商、机构提供的加速镜像,来提高拉取镜像的速度。但是随着Docker hub限制了未注册用户的拉取频率、各大厂商、机构开始将加速镜像转为内部使用,个人用户拉取镜像变得越来越困难。在长期拉取镜像速度看脸的头疼之下,尝试通过 Nginx 和 Cloudflare Worker 两种方案以及两种方案的组合方案自建Docker hub加速镜像来解决这个问题

2024-04-16 20:21

schedule

28 min

1 second.

更新日志

2024 年 09 月 12 日

auth.docker.io 被墙,增加反代 auth.docker.io 的配置

官方的镜像仓库无论是否登录,都会要求认证,而当我们使用反代了官方仓库的镜像源拉取镜像时,官方仓库会首先返回一个 401 Unauthorized 响应,并附带一个 WWW-Authenticate 头,其中包含了获取认证所需的服务地址和授权范围(scope)。而这个“获取认证所需的服务地址”默认是 auth.docker.io。但是这个域名今天已经被封禁了,进而影响了我们拉取镜像。

在经过测试后发现可以通过重写这一 WWW-Authenticate 头,将认证请求导向我们自己的反代,交由反代服务器去访问 auth.docker.io,这样就规避了对 auth.docker.io 的封禁。

可选的加速方案

使用公共的加速镜像 国内的 Docker Hub 镜像加速器,由国内教育机构与各大云服务商提供的镜像加速服务

速度通常不错,但由于 Docker hub 限制了未注册用户的拉取频率,可能会导致加速镜像无法及时同步最新的镜像(例如 阿里云镜像源 )

由于很高的成本,可能会导致一些原本提供了镜像源的厂商、机构停止提供(如 163、七牛云等)或转向仅为内网内的设备和用户提供服务(如腾讯云、 中国科学技术大学开源软件镜像 等)

使用代理拉取镜像 如何配置 docker 通过代理服务器拉取镜像 。简单来说就是通过设置由 systemd 管理的 docker daemon 的环境变量来实现利用代理拉取镜像

优点自然就是带宽几乎没有限制,并且镜像不会缓存,不会下载到未更新的镜像

缺点是需要设备上或局域网内有现成的代理且一些特殊设备(例如威联通、群晖)经过了厂商魔改难以手动设置环境变量,没有加速镜像的通用型强,需要每个设备单独设置

间接拉取镜像。在境外机器已经配置了代理的设备上拉取镜像,再将镜像导出、传输到需要使用镜像的设备上、最后导入。

优点…能用

缺点自然就是很麻烦,需要提前准备好传输文件的方案

最后整合上面我使用过的几个拉取镜像的方案,最后的选择是自建一个加速镜像,免得以后的到处找能用的加速镜像源

自建加速镜像

在尝试搭建之前找了挺多资料,主流的方式有:使用官方提供的 registry,第三方的 Nexus、Harbor。但是使用 registry 搭建一直没有成功,客户端一直报找不到指定镜像;使用 Nexus 搭建又有些太复杂。最后自己总结出来了这两个比较方便的方案。

一点小发现:

在使用 Nginx 搭建时,发现服务器的流量很小,经过检查 Nginx 的日志后发现,Docker hub 镜像仓库返回的下载地址是需要 307 跳转的,而跳转后的地址直连依然下载很慢,所以需要在服务端处理这个跳转,由服务端请求并将跳转后的数据返回客户端。

方案一:使用 Nginx 搭建

系统:Ububtu 22.04 服务器:Zgocloud 洛杉矶 9929+CMIN2

提到要加速一个网站,自然就能想到使用 Nginx 反代一下了。接下来是具体的配置方案

安装 Nginx

sudo apt update sudo apt install nginx

防火墙放行指定端口

我这里使用的防火墙是系统自带的 UFW,并且没有开厂商提供的防火墙(忘记是关掉了还是本来就没有,反正没开),所以只需要 sudo ufw allow ‘Nginx Full’ 一条命令即可,这样就会放行 IPv4 和 IPv6 的 80 和 443 端口(当然也可以手动 sudo ufw allow 443 这样只开放 443 端口)

如果没有使用防火墙,就不用设置这一步,如果还使用了厂商提供的防火墙,就需要在厂商的面板处同样开放这些端口

配置 Nginx 使用命令 sudo vim /etc/nginx/nginx.conf 编辑 Nginx 配置,在 http 块下增加一个 server 块

/etc/nginx/nginx.conf

#反代docker hub镜像源 server { listen 443 ssl; server_name 域名;

ssl_certificate 证书地址; ssl_certificate_key 密钥地址;

ssl_session_timeout 24h; ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'; ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;

location /v2/ { proxy_pass https://registry-1.docker.io; # Docker Hub 的官方镜像仓库 proxy_set_header Host registry-1.docker.io; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;

关闭缓存

proxy_buffering off;

转发认证相关的头部

proxy_set_header Authorization $http_authorization; proxy_pass_header Authorization;

重写 www-authenticate 头为你的反代地址

proxy_hide_header www-authenticate; add_header www-authenticate 'Bearer realm="https://域名/token",service="registry.docker.io"' always;

always 参数确保该头部在返回 401 错误时无论什么情况下都会被添加。

对 upstream 状态码检查,实现 error_page 错误重定向

proxy_intercept_errors on;

error_page 指令默认只检查了第一次后端返回的状态码,开启后可以跟随多次重定向。

recursive_error_pages on;

根据状态码执行对应操作,以下为301、302、307状态码都会触发

error_page 301 302 307 = @handle_redirect;

}

处理 Docker OAuth2 Token 认证请求

location /token { resolver 1.1.1.1 valid=600s; proxy_pass https://auth.docker.io; # Docker 认证服务器

设置请求头,确保转发正确

proxy_set_header Host auth.docker.io; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;

传递 Authorization 头信息,获取 Token

proxy_set_header Authorization $http_authorization; proxy_pass_header Authorization;

禁用缓存

proxy_buffering off; } location @handle_redirect { resolver 1.1.1.1; set $saved_redirect_location '$upstream_http_location'; proxy_pass $saved_redirect_location; } }

然后按 Esc ,输入 : wq 保存退出即可

重新加载 Nginx 配置 输入命令 sudo nginx -s reload ,没有报错就说明配置已经生效

方案二:使用 CloudFlare Worker 搭建

作为一个贫穷(doge)的用户,可以免费使用的 CloudFlare Worker 自然要想方设法的用用了,虽然 CloudFlare Worker 的访问速度在国内也不算稳定,但在 CloudFlare 的边缘网络的加持下,白天的速度还是非常可观的,晚上会比较慢但还是比直接使用官方的镜像源要快上很多(又不要钱,要啥自行车.jpg)

这里是使用的 基于 Cloudflare Worker 的容器镜像加速器 ,更详细的教程可以参考 2024 自建 Docker 镜像代理加速 教程来了 3 分钟部署完毕

简单教程:

在面板左侧找到 Workers 和 Pages ,然后点击右侧的 创建应用程序 、 创建 Worker ,修改一个好记的名字, 部署

接下来编辑代码,将 worker.js 的内容替换为下面内容,并且修改 工作服务器地址 为一个 dns 托管在 cloudflare 的域名,例如 let workers_url = ‘ https://lty520.faith/';

worker.js

// _worker.js

// Docker镜像仓库主机地址 let hub_host = 'registry-1.docker.io'; // Docker认证服务器地址 const auth_url = 'https://auth.docker.io'; // 自定义的工作服务器地址 let workers_url = 'https://xxx/';

let 屏蔽爬虫UA = ['netcraft'];

// 根据主机名选择对应的上游地址 function routeByHosts(host) { // 定义路由表 const routes = { // 生产环境 "quay": "quay.io", "gcr": "gcr.io", "k8s-gcr": "k8s.gcr.io", "k8s": "registry.k8s.io", "ghcr": "ghcr.io", "cloudsmith": "docker.cloudsmith.io", "nvcr": "nvcr.io",

// 测试环境 "test": "registry-1.docker.io", };

if (host in routes) return [ routes[host], false ]; else return [ hub_host, true ]; }

/** @type {RequestInit} / const PREFLIGHT_INIT = { // 预检请求配置 headers: new Headers({ 'access-control-allow-origin': '', // 允许所有来源 'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS', // 允许的HTTP方法 'access-control-max-age': '1728000', // 预检请求的缓存时间 }), }

/**

  • 构造响应
  • @param {any} body 响应体
  • @param {number} status 响应状态码
  • @param {Object<string, string>} headers 响应头 / function makeRes(body, status = 200, headers = {}) { headers['access-control-allow-origin'] = '' // 允许所有来源 return new Response(body, { status, headers }) // 返回新构造的响应 }

/**

  • 构造新的URL对象
  • @param {string} urlStr URL字符串 */ function newUrl(urlStr) { try { return new URL(urlStr) // 尝试构造新的URL对象 } catch (err) { return null // 构造失败返回null } }

function isUUID(uuid) { // 定义一个正则表达式来匹配 UUID 格式 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;

// 使用正则表达式测试 UUID 字符串 return uuidRegex.test(uuid); }

async function nginx() { const text = `

async function searchInterface() { const text = `

document.getElementById('search-button').addEventListener('click', performSearch); document.getElementById('search-input').addEventListener('keypress', function(event) { if (event.key === 'Enter') { performSearch(); } });

export default { async fetch(request, env, ctx) { const getReqHeader = (key) => request.headers.get(key); // 获取请求头

let url = new URL(request.url); // 解析请求URL const userAgentHeader = request.headers.get('User-Agent'); const userAgent = userAgentHeader ? userAgentHeader.toLowerCase() : "null"; if (env.UA) 屏蔽爬虫UA = 屏蔽爬虫UA.concat(await ADD(env.UA)); workers_url = https://${url.hostname}; const pathname = url.pathname;

// 获取请求参数中的 ns const ns = url.searchParams.get('ns'); const hostname = url.searchParams.get('hubhost') || url.hostname; const hostTop = hostname.split('.')[0]; // 获取主机名的第一部分

let checkHost; // 在这里定义 checkHost 变量 // 如果存在 ns 参数,优先使用它来确定 hub_host if (ns) { if (ns === 'docker.io') { hub_host = 'registry-1.docker.io'; // 设置上游地址为 registry-1.docker.io } else { hub_host = ns; // 直接使用 ns 作为 hub_host } } else { checkHost = routeByHosts(hostTop); hub_host = checkHost[0]; // 获取上游地址 }

const fakePage = checkHost ? checkHost[1] : false; // 确保 fakePage 不为 undefined console.log(域名头部: ${hostTop} 反代地址: ${hub_host} 伪装首页: ${fakePage}); const isUuid = isUUID(pathname.split('/')[1].split('/')[0]);

if (屏蔽爬虫UA.some(fxxk => userAgent.includes(fxxk)) && 屏蔽爬虫UA.length > 0) { // 首页改成一个nginx伪装页 return new Response(await nginx(), { headers: { 'Content-Type': 'text/html; charset=UTF-8', }, }); }

const conditions = [ isUuid, pathname.includes('/_'), pathname.includes('/r/'), pathname.includes('/v2/user'), pathname.includes('/v2/orgs'), pathname.includes('/v2/_catalog'), pathname.includes('/v2/categories'), pathname.includes('/v2/feature-flags'), pathname.includes('search'), pathname.includes('source'), pathname === '/', pathname === '/favicon.ico', pathname === '/auth/profile', ];

if (conditions.some(condition => condition) && (fakePage === true || hostTop == 'docker')) { if (env.URL302) { return Response.redirect(env.URL302, 302); } else if (env.URL) { if (env.URL.toLowerCase() == 'nginx') { //首页改成一个nginx伪装页 return new Response(await nginx(), { headers: { 'Content-Type': 'text/html; charset=UTF-8', }, }); } else return fetch(new Request(env.URL, request)); } else if (url.pathname == '/'){ return new Response(await searchInterface(), { headers: { 'Content-Type': 'text/html; charset=UTF-8', }, }); }

const newUrl = new URL("https://registry.hub.docker.com" + pathname + url.search);

// 复制原始请求的标头 const headers = new Headers(request.headers);

// 确保 Host 头部被替换为 hub.docker.com headers.set('Host', 'registry.hub.docker.com');

const newRequest = new Request(newUrl, { method: request.method, headers: headers, body: request.method !== 'GET' && request.method !== 'HEAD' ? await request.blob() : null, redirect: 'follow' });

return fetch(newRequest); }

// 修改包含 %2F 和 %3A 的请求 if (!/%2F/.test(url.search) && /%3A/.test(url.toString())) { let modifiedUrl = url.toString().replace(/%3A(?=.*?&)/, '%3Alibrary%2F'); url = new URL(modifiedUrl); console.log(handle_url: ${url}); }

// 处理token请求 if (url.pathname.includes('/token')) { let token_parameter = { headers: { 'Host': 'auth.docker.io', 'User-Agent': getReqHeader("User-Agent"), 'Accept': getReqHeader("Accept"), 'Accept-Language': getReqHeader("Accept-Language"), 'Accept-Encoding': getReqHeader("Accept-Encoding"), 'Connection': 'keep-alive', 'Cache-Control': 'max-age=0' } }; let token_url = auth_url + url.pathname + url.search; return fetch(new Request(token_url, request), token_parameter); }

// 修改 /v2/ 请求路径 if ( hub_host == 'registry-1.docker.io' && /^/v2/[^/]+/[^/]+/[^/]+$/.test(url.pathname) && !/^/v2/library/.test(url.pathname)) { //url.pathname = url.pathname.replace(//v2//, '/v2/library/'); url.pathname = '/v2/library/' + url.pathname.split('/v2/')[1]; console.log(modified_url: ${url.pathname}); }

// 更改请求的主机名 url.hostname = hub_host;

// 构造请求参数 let parameter = { headers: { 'Host': hub_host, 'User-Agent': getReqHeader("User-Agent"), 'Accept': getReqHeader("Accept"), 'Accept-Language': getReqHeader("Accept-Language"), 'Accept-Encoding': getReqHeader("Accept-Encoding"), 'Connection': 'keep-alive', 'Cache-Control': 'max-age=0' }, cacheTtl: 3600 // 缓存时间 };

// 添加Authorization头 if (request.headers.has("Authorization")) { parameter.headers.Authorization = getReqHeader("Authorization"); }

// 发起请求并处理响应 let original_response = await fetch(new Request(url, request), parameter); let original_response_clone = original_response.clone(); let original_text = original_response_clone.body; let response_headers = original_response.headers; let new_response_headers = new Headers(response_headers); let status = original_response.status;

// 修改 Www-Authenticate 头 if (new_response_headers.get("Www-Authenticate")) { let auth = new_response_headers.get("Www-Authenticate"); let re = new RegExp(auth_url, 'g'); new_response_headers.set("Www-Authenticate", response_headers.get("Www-Authenticate").replace(re, workers_url)); }

// 处理重定向 if (new_response_headers.get("Location")) { return httpHandler(request, new_response_headers.get("Location")); }

// 返回修改后的响应 let response = new Response(original_text, { status, headers: new_response_headers }); return response; } };

/**

  • 处理HTTP请求
  • @param {Request} req 请求对象
  • @param {string} pathname 请求路径 */ function httpHandler(req, pathname) { const reqHdrRaw = req.headers;

// 处理预检请求 if (req.method === 'OPTIONS' && reqHdrRaw.has('access-control-request-headers') ) { return new Response(null, PREFLIGHT_INIT); }

let rawLen = '';

const reqHdrNew = new Headers(reqHdrRaw);

const refer = reqHdrNew.get('referer');

let urlStr = pathname;

const urlObj = newUrl(urlStr);

/** @type {RequestInit} */ const reqInit = { method: req.method, headers: reqHdrNew, redirect: 'follow', body: req.body }; return proxy(urlObj, reqInit, rawLen); }

/**

  • 代理请求
  • @param {URL} urlObj URL对象
  • @param {RequestInit} reqInit 请求初始化对象
  • @param {string} rawLen 原始长度 */ async function proxy(urlObj, reqInit, rawLen) { const res = await fetch(urlObj.href, reqInit); const resHdrOld = res.headers; const resHdrNew = new Headers(resHdrOld);

// 验证长度 if (rawLen) { const newLen = resHdrOld.get('content-length') || ''; const badLen = (rawLen !== newLen);

if (badLen) { return makeRes(res.body, 400, { '--error': bad len: ${newLen}, except: ${rawLen}, 'access-control-expose-headers': '--error', }); } } const status = res.status; resHdrNew.set('access-control-expose-headers', ''); resHdrNew.set('access-control-allow-origin', ''); resHdrNew.set('Cache-Control', 'max-age=1500');

// 删除不必要的头 resHdrNew.delete('content-security-policy'); resHdrNew.delete('content-security-policy-report-only'); resHdrNew.delete('clear-site-data');

return new Response(res.body, { status, headers: resHdrNew }); }

async function ADD(envadd) { var addtext = envadd.replace(/[ |"'\r ]+/g, ',').replace(/,+/g, ','); // 将空格、双引号、单引号和换行符替换为逗号 if (addtext.charAt(0) == ',') addtext = addtext.slice(1); if (addtext.charAt(addtext.length - 1) == ',') addtext = addtext.slice(0, addtext.length - 1); const add = addtext.split(','); return add; }

TEXT

接下来,点击右上角的 部署 ,稍等片刻 

最后,返回面板,在 设置 , 触发器 处设置一个自己的域名,一切就大功告成了

不建议使用自带的 workers.dev 的域名,被墙了 

方案一、二整合 

本来上面的两个方案是独立的,一个使用 Nginx 部署,一个使用 CloudFlare Worker 部署,但是就在我写这篇博客的时候,突然想到,为什么不能把上面的两个方案整合起来呢?

利用服务器搭建的 Nginx 作为中转,优先由服务器直连 Docker hub 的官方仓库,当服务器的 IP 请求次数超限后(会报 429 错误),就把请求转发到 CloudFlare Worker 部署的镜像源上,利用 CloudFlare Worker 再做一次中转。这样就即保证了使用服务器中转提高速度,又保证了不会因为服务器的 IP 请求速度过多而受限制,唯一的限制就是服务器的带宽和流量了,几乎完美!!! 

部署方法:

将上面部署的 Nginx 配置替换为下面的配置并使用 sudo nginx -s reload 重新加载即可 

/etc/nginx/nginx.conf 

#反代docker hub镜像源
server {
listen 443 ssl;
server_name 域名;

ssl_certificate 证书地址;
ssl_certificate_key 密钥地址;

proxy_ssl_server_name on; # 启用SNI

ssl_session_timeout 24h;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;

location /v2/ {
proxy_pass https://registry-1.docker.io; # Docker Hub 的官方镜像仓库

proxy_set_header Host registry-1.docker.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# 关闭缓存
proxy_buffering off;

# 转发认证相关的头部
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;

# 重写 www-authenticate 头为你的反代地址
proxy_hide_header www-authenticate;
add_header www-authenticate 'Bearer realm="https://域名/token",service="registry.docker.io"' always;
# always 参数确保该头部在返回 401 错误时无论什么情况下都会被添加。

# 对 upstream 状态码检查,实现 error_page 错误重定向
proxy_intercept_errors on;
# error_page 指令默认只检查了第一次后端返回的状态码,开启后可以跟随多次重定向。
recursive_error_pages on;
# 根据状态码执行对应操作,以下为301、302、307状态码都会触发
#error_page 301 302 307 = @handle_redirect;

error_page 429 = @handle_too_many_requests;
}
# 处理 Docker OAuth2 Token 认证请求
location /token {
resolver 1.1.1.1 valid=600s;
proxy_pass https://auth.docker.io; # Docker 认证服务器

# 设置请求头,确保转发正确
proxy_set_header Host auth.docker.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# 传递 Authorization 头信息,获取 Token
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;

# 禁用缓存
proxy_buffering off;
}
#处理重定向
location @handle_redirect {
resolver 1.1.1.1;
set $saved_redirect_location '$upstream_http_location';
proxy_pass $saved_redirect_location;
}
# 处理429错误
location @handle_too_many_requests {
proxy_set_header Host 替换为在CloudFlare Worker设置的域名; # 替换为另一个服务器的地址
proxy_pass http://替换为在CloudFlare Worker设置的域名;
proxy_set_header Host $http_host;
}
}

如果想要反代 ghcr 镜像源呢?只要参考上面的配置,将域名、header 修改一下即可

并且因为 ghcr 好像不像 docker hub 有下载频率的限制,所以也不用去 Cloudflare Worker 部署了,直接在服务器上部署一个就行。 

#反代ghcr镜像源
server {
listen 443 ssl;
server_name 域名;

ssl_certificate 证书地址;
ssl_certificate_key 密钥地址;
proxy_ssl_server_name on;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
#error_log /home/ubuntuago/proxy_docker.log debug;
if ($blocked_agent) {
return 403;
}

location / {
proxy_pass https://ghcr.io; # Docker Hub 的官方镜像仓库

proxy_set_header Host ghcr.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# 关闭缓存
proxy_buffering off;

# 转发认证相关的头部
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
# 对 upstream 状态码检查,实现 error_page 错误重定向
proxy_intercept_errors on;
# error_page 指令默认只检查了第一次后端返回的状态码,开启后可以跟随多次重定向。
recursive_error_pages on;
# 根据状态码执行对应操作,以下为301、302、307状态码都会触发
error_page 301 302 307 = @handle_redirect;

#error_page 429 = @handle_too_many_requests;

}
#处理重定向
location @handle_redirect {
resolver 1.1.1.1;
set $saved_redirect_location '$upstream_http_location';
proxy_pass $saved_redirect_location;
}

}

新增的一些代码的作用: 

proxy_ssl_server_name on; 

HTTPS, 需要在握手时候验证证书, 所以在握手时候需要将域名告诉对方, 找到匹配的证书, 这就是 SNI 的工作. 否则会导致证书找不到而请求失败.

默认情况下, nginx 并不会开启 proxy_ssl_server_name, 也就是说不启用 SNI. 如果使用 nginx 反代一个虚拟主机的服务, 比如 Cloudflare Workers, 此时如果不开启 SNI, 会导致与 CF 握手时候, CF 并不清楚请求哪一个域名下的服务, 所以找不到匹配的证书, 因此会报 502 错误. 当然也可以使用 proxy_ssl_name 字段复写于最终服务器收到的域名.

引自: SNI 

在尝试整合这两个方案的时候不知道这个参数,一直报 502..人都麻了 

error_page 429 = @handle_too_many_requests; 

当错误代码为 429 时(即请求次数超限)转发到在 Cloudflare Workers 部署的镜像源 

proxy_set_header Host 替换为在 CloudFlare Worker 设置的域名; 

将发给 CloudFlare Worker 的请求加上正确的域名方可让请求到达我们搭建的镜像源,如果不加会报 404 

镜像源使用方式 

配置镜像源的方式在网上有很多,这里不做赘述

但是使用“方案一”、“方案一、二整合”的方式搭建后,如果要使用我们的反代登录到官方仓库,应在 docker login 后增加反代服务器的域名,例如 docker login example.com 然后再正常输入 docker hub 的用户名、密码。 

注意!如果利用反代服务器登录 docker hub 仓库,反代服务器是可以获取到你的账号密码的,所以请谨慎使用他人搭建的服务登录! 

如果没有拉取自己的私有仓库的镜像的需求,保持匿名拉取相对会安全一些。 

各种加速镜像实测 

测试系统为 Debian12,测试镜像为 uptime-kuma: latest 

使用命令 time docker pull 加速镜像地址/louislam/uptime-kuma 测试 

测试时间为 2024 年 04 月 16 日 22 时 

dockerproxy.com Docker 镜像代理

用时 19.370s 

docker.m.daocloud.io DaoCloud 镜像站

用时 13.927s 

docker.mirrors.sjtug.sjtu.edu.cn 上海交大镜像站

用时 12.157s 

docker.nju.edu.cn 南京大学镜像站

用时 12.690s 

Nginx 反代自建方案

处理重定向

晚上十点测试,用时 16.342s 

次日下午五点测试,用时 15.432s 

不处理重定向

次日下午五点测试,虽然不会出现使用官方仓库一样的卡死,但极慢 

CloudFlare Worker 反代自建方案

上午十一点测试,用时 15.731s 

晚高峰几乎无法使用 

直连官方镜像仓库

几乎无法使用,完全看脸 

完全无法使用 

总结 

根据测试,自建一个 Docker hub 加速镜像完全可行,使用 Nginx 搭建会比较看重服务器的线路,但只要不算太差,就完全可用了。不处理跳转会比较节省流量,但是为了速度,节省这一点流量也没什么必要。有一定的成本,但是如果本身就已经买了服务器的话,搭建个镜像源就几乎是顺带的事情了。

而使用 CloudFlare Worker 搭建..只能说稍微好一点点,如果服务器需要大量拉取镜像的话或许会用到这个方案。

而将方案一、二整合,几乎就是现阶段最完美的方案了,速度快且不受拉取次数的限制,只要 Docker hub 别乱修改拉取镜像的流程,理论上可以用超久。 

参考文章 

如何配置 docker 通过代理服务器拉取镜像 

绕过从 Docker Hub pull 镜像时的 429 toomanyrequests 

怎么搭建自己的 Docker-hub 镜像源,本地搭一个得了 自己用! 

基于 Cloudflare Worker 的容器镜像加速器 

Docker Hub rate limit 

国内的 Docker Hub 镜像加速器,由国内教育机构与各大云服务商提供的镜像加速服务 

利用 nginx 实现 302 重定向自动跟随 

Nginx 反向代理 SSL 站点出现 502 报错的解决办法 

SNI 

Nginx 反向代理,当后端为 Https 时的一些细节和原理 

最后修改于 2024-04-16 

下回
友链

上回
使用脚本轻松消耗流量

昵称 
邮箱 
网址 

0 字 登录 提交 

34 评论 
按正序
按倒序
按热度 

loren 2025-02-27 
0 
湖北 Edge133.0 Windows 11 

Unable to find image ‘ xxx.xxx.com/raywangqvq/bilibili_tool_pro:latest ’ locally

docker: Error response from daemon: unauthorized: authentication required.

See ‘docker run --help’. 

其中xxx.xxx.com是代理网址,之前都正常,最近一周开始无法拉取。报错。 

匿名 2025-02-26 
0 
香港 Chrome133.0 Mac OS 10.15.7 

这是什么原因啊博主

$docker pull mysql:latest

latest: Pulling from library/mysql

5645ac95474c: Pulling fs layer

2adf46058f45: Pulling fs layer

c5d32318ef6d: Pulling fs layer

666d78c46a16: Pulling fs layer

bcba864ffae7: Pulling fs layer

failed to copy: httpReadSeeker: failed open: unexpected status code https://proxy.xxxx.com/v2/library/mysql/blobs/sha256:bcba864ffae72cc219d450a33749416d0fc52787a1a6eb4fa2c663f84fa2a623?ns=docker.io: 403 Forbidden 

shyandsy 2025-02-26 
0 
香港 Chrome133.0 Mac OS 10.15.7 

@匿名 : 我的配置如下

server

{

listen 80;

listen 443 ssl ;

server_name proxy.xxx.com ;

#index index.php index.html index.htm default.php default.htm default.html;

root /www/wwwroot/proxy.xxxx.com;

#CERT-APPLY-CHECK–START

# 用于SSL证书申请时的文件验证相关配置 – 请勿删除

include /www/server/panel/vhost/nginx/well-known/proxy.xxx.com.conf;

#CERT-APPLY-CHECK–END 

#SSL-START SSL相关配置,请勿删除或修改下一行带注释的404规则
#error_page 404/404.html;
ssl_certificate /www/server/panel/vhost/cert/proxy.xxx.com/fullchain.pem;
ssl_certificate_key /www/server/panel/vhost/cert/proxy.xxx.com/privkey.pem;
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
ssl_ciphers EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers on;
ssl_session_tickets on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 24h;
#add_header Strict-Transport-Security "max-age=31536000";
#error_page 497 https://$host$request_uri;

#proxy_set_header X-Forwarded-Proto https;
#proxy_set_header Host $host;
#proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#proxy_set_header X-Forwarded-Host $server_name;
#proxy_set_header X-Real-IP $remote_addr;
#proxy_http_version 1.1;
#proxy_set_header Upgrade $http_upgrade;
#proxy_set_header Connection $http_connection;

#SSL-END

#ERROR-PAGE-START 错误页配置,可以注释、删除或修改
error_page 404 /404.html;
#error_page 502 /502.html;
#ERROR-PAGE-END

#PHP-INFO-START PHP引用配置,可以注释或修改
#include enable-php-00.conf;
#PHP-INFO-END

#REWRITE-START URL重写规则引用,修改后将导致面板设置的伪静态规则失效
include /www/server/panel/vhost/rewrite/proxy.xxx.com.conf;
#REWRITE-END

#禁止访问的文件或目录
location ~ ^/(\.user.ini|\.htaccess|\.git|\.env|\.svn|\.project|LICENSE|README.md)
{
return 404;
}

#一键申请SSL证书验证目录相关设置
location ~ \.well-known{
allow all;
}

#禁止在证书验证目录放入敏感文件
if ( $uri ~ "^/\.well-known/.*\.(php|jsp|py|js|css|lua|ts|go|zip|tar\.gz|rar|7z|sql|bak)$" ) {
return 403;
}

#location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
#{
# expires 30d;
# error_log /dev/null;
# access_log /dev/null;
#}

#location ~ .*\.(js|css)?$
#{
# expires 12h;
# error_log /dev/null;
# access_log /dev/null;
#}
access_log /www/wwwlogs/proxy.xxx.com.log;
error_log /www/wwwlogs/proxy.xxx.com.error.log;

# 重定向http到https
if ($scheme = http) {
return 301 https://$host$request_uri;
}

location /v2/ {
proxy_pass https://registry-1.docker.io; # Docker Hub 的官方镜像仓库
proxy_set_header Host registry-1.docker.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# 关闭缓存
proxy_buffering off;

# 转发认证相关的头部
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;

# 重写 www-authenticate 头为你的反代地址
proxy_hide_header www-authenticate;
add_header www-authenticate 'Bearer realm="https://proxy.xxx.com/token",service="registry.docker.io"' always;
# always 参数确保该头部在返回 401 错误时无论什么情况下都会被添加。

# 对 upstream 状态码检查,实现 error_page 错误重定向
proxy_intercept_errors on;
# error_page 指令默认只检查了第一次后端返回的状态码,开启后可以跟随多次重定向。
recursive_error_pages on;
# 根据状态码执行对应操作,以下为301、302、307状态码都会触发
error_page 301 302 307 = @handle_redirect;

}
# 处理 Docker OAuth2 Token 认证请求
location /token {
resolver 1.1.1.1 valid=600s;
proxy_pass https://auth.docker.io; # Docker 认证服务器

# 设置请求头,确保转发正确
proxy_set_header Host auth.docker.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# 传递 Authorization 头信息,获取 Token
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;

# 禁用缓存
proxy_buffering off;
}
location @handle_redirect {
resolver 1.1.1.1;
set $saved_redirect_location '$upstream_http_location';
proxy_pass $saved_redirect_location;
}

} 

匿名 2024-12-31 
0 
弗吉尼亚州 Chrome126.0 Windows 10 

请教一下,今天GHCR拉取镜像报错,Error response from daemon: Head “ https://ghcr.xxx.com/v2/paperless-ngx/paperless-ngx/manifests/latest ”: Get “ https://ghcr.io/token?scope=repository%3Apaperless-ngx%2Fpaperless-ngx%3Apull&service=ghcr.io ”: read tcp 10.0.0.11:46076->20.205.243.164:443: read: connection reset by peer 但是ng日志请求显示又是200:“HEAD /v2/gotenberg/gotenberg/manifests/8.7 HTTP/1.1” 200 0 “-” “docker/20.10.23 go/go1.19.1 git-commit/5fcb1cf kernel/4.4.302+ os/linux arch/amd64 UpstreamClient(Docker-Client/unknown-version \x5C(linux\x5C))” 

匿名 2024-10-07 
0 
洛杉矶 Chrome129.0 Windows 10 

你好博主,我想将我的nas用外部服务器做一个nginx反代,目前是这个配置 

location / {

proxy_pass https://registry.hub.docker.com;
#不增加此代理转发获取不到镜像列表
proxy_set_header Host registry.hub.docker.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
client_max_body_size 1024M;
}

location /v2/ {
proxy_pass https://registry-1.docker.io;
proxy_set_header Host registry-1.docker.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

proxy_buffering off;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;

proxy_hide_header www-authenticate;
add_header www-authenticate 'Bearer realm="打码",service="registry.docker.io"' always;

proxy_intercept_errors on;
recursive_error_pages on;
error_page 301 302 307 = @handle_redirect;
}

但是nginx的连接日志是这样的 

60.177.225.143 - - [07/Oct/2024:12:36:35 +0800] “GET /v2/_catalog HTTP/2.0” 404 19 “-” “-”

60.177.225.143 - - [07/Oct/2024:12:36:37 +0800] “GET /v1/search?q=library/&n=50&page=1 HTTP/2.0” 503 108 “-” “-”

60.177.225.143 - - [07/Oct/2024:12:36:58 +0800] “GET /v2/_catalog HTTP/2.0” 404 19 “-” “-”

60.177.225.143 - - [07/Oct/2024:12:36:58 +0800] “GET /v1/search?q=library/&n=50&page=1 HTTP/2.0” 503 108 “-” “-”

60.177.225.143 - - [07/Oct/2024:12:37:56 +0800] “GET /v2/_catalog HTTP/2.0” 404 19 “-” “-”

60.177.225.143 - - [07/Oct/2024:12:37:57 +0800] “GET /v1/search?q=library/&n=50&page=1 HTTP/2.0” 200 10622 “-” “-”

60.177.225.143 - - [07/Oct/2024:12:38:02 +0800] “GET /v2/nginx/tags/list HTTP/2.0” 404 19 “-” “-”

60.177.225.143 - - [07/Oct/2024:12:38:03 +0800] “GET /v1/repositories/nginx/tags HTTP/2.0” 410 140 “-” “-”

60.177.225.143 - - [07/Oct/2024:12:39:51 +0800] “GET /v2/nginx/tags/list HTTP/2.0” 401 148 “-” “-”

60.177.225.143 - - [07/Oct/2024:12:39:52 +0800] “GET /v1/repositories/nginx/tags HTTP/2.0” 410 140 “-” “-”

60.177.225.143 - - [07/Oct/2024:12:40:48 +0800] “GET /v2/nginx/tags/list HTTP/2.0” 401 148 “-” “-”

有遇到过这种情况吗 

匿名 2024-10-05 
0 
Chrome129.0 Windows 11 

你好博主,我是直接使用的你的nginx配置反代,可以看一下我的问题如何解决么?

root@172-20-2-19:~/alist# docker compose pull

WARN[0000] /root/alist/docker-compose.yml: the attribute version is obsolete, it will be ignored, please remove it to avoid potential confusion

[+] Pulling 1/1

✘ alist Error Get “ https://registry-1.docker.io/v2/ ”: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awai… 17.7s

Error response from daemon: Get “ https://registry-1.docker.io/v2/ ”: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers) 

匿名 2024-10-05 
0 
Chrome129.0 Windows 11 

@匿名 : 解决了。nginx配置文件中有个地方“域名”改为自己的域名可以了,当时没仔细看,以为是注释,打扰了,感谢博主的文章 

Anonymous 2024-10-04 
0 
悉尼 Edge129.0 Windows 11 

您好,用了方案一的 Nginx 配置访问 /v2/ 还是 401 UNAUTHORIZED 

匿名 2025-04-20 
0 
伦敦 Chrome135.0 Windows 10 

@Anonymous : 已解决 

ninja 2024-10-01 
0 
Chrome118.0 Android 14 

你好我用的是openresty搭建的反向代理,在另一台机子上拉镜像报错如下

stderr: ntfy Pulling 4abcf2066143 Pulling fs layer b7e3795d4084 Pulling fs layer 08fafcd928bb Pulling fs layer Get “ https://registry-1.docker.io/v2/ ”: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers) 

xy3 2024-09-19 
0 
纽约 Edge127.0 Windows 11 

Uncaught SyntaxError: Unexpected token ‘export’ at worker.js:191:1

如何解决,复制的博客代码 

xy3 2024-09-19 
0 
纽约 Edge127.0 Windows 11 

@xy3 : 解决了,删掉woker重建就好 

匿名 2024-09-14 
0 
洛杉矶 Chrome128.0 Windows 11 

[root@iZbp11r55xkekh1tyesl8kZ exam]# docker pull docker.xx.cn/maven 

Using default tag: latest

Error response from daemon: unauthorized: authentication required 

docker logs -f nginx-proxy

/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration

/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/

/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh

10-listen-on-ipv6-by-default.sh : info: Getting the checksum of /etc/nginx/conf.d/default.conf

10-listen-on-ipv6-by-default.sh : info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf

/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh

/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh

/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh

/docker-entrypoint.sh: Configuration complete; ready for start up

2024/09/14 09:03:32 [notice] 1#1: using the “epoll” event method

2024/09/14 09:03:32 [notice] 1#1: nginx/1.27.1

2024/09/14 09:03:32 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14)

2024/09/14 09:03:32 [notice] 1#1: OS: Linux 5.15.60-1.el7.x86_64

2024/09/14 09:03:32 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576

2024/09/14 09:03:32 [notice] 1#1: start worker processes

2024/09/14 09:03:32 [notice] 1#1: start worker process 28

47.98.39.69 - - [14/Sep/2024:09:03:37 +0000] “GET /v2/ HTTP/1.1” 401 87 “-” “docker/23.0.1 go/go1.19.5 git-commit/bc3805a kernel/5.10.134-13.1.al8.x86_64 os/linux arch/amd64 UpstreamClient(Docker-Client/23.0.1 \x5C(linux\x5C))” “-”

47.98.39.69 - pengfeiu [14/Sep/2024:09:03:38 +0000] “GET /token?account=pengfeiu&scope=repository%3Amaven%3Apull& service=registry.docker.io HTTP/1.1” 200 5364 “-” “docker/23.0.1 go/go1.19.5 git-commit/bc3805a kernel/5.10.134-13.1.al8.x86_64 os/linux arch/amd64 UpstreamClient(Docker-Client/23.0.1 \x5C(linux\x5C))” “-”

47.98.39.69 - - [14/Sep/2024:09:03:39 +0000] “HEAD /v2/maven/manifests/latest HTTP/1.1” 401 0 “-” “docker/23.0.1 go/go1.19.5 git-commit/bc3805a kernel/5.10.134-13.1.al8.x86_64 os/linux arch/amd64 UpstreamClient(Docker-Client/23.0.1 \x5C(linux\x5C))” “-”

47.98.39.69 - - [14/Sep/2024:09:03:39 +0000] “GET /v2/maven/manifests/latest HTTP/1.1” 401 148 “-” “docker/23.0.1 go/go1.19.5 git-commit/bc3805a kernel/5.10.134-13.1.al8.x86_64 os/linux arch/amd64 UpstreamClient(Docker-Client/23.0.1 \x5C(linux\x5C))” “-” 

Anonymous 2024-09-13 
1 
东京都 Chrome126.0 Windows 11 

今天刚好 401,帮了个大忙。 

加载更多... 
Powered by Waline v2.15.8 

Ling的博客

博文

随记

CTF

友链

RSS Feed

Hugo Theme Diary by Rise 

移植自 Makito 's Journal. 

©

2025 Ling的博客

- 目录 - 

更新日志

可选的加速方案

自建加速镜像

方案一:使用 Nginx 搭建

方案二:使用 CloudFlare Worker 搭建

方案一、二整合

镜像源使用方式

各种加速镜像实测

总结

参考文章

keyboard_arrow_up

light_mode 

Hugo Theme by 
移植自 's 
©

2025 Ling的博客

相关文章