路易吉
发布于 2024-06-10 / 334 阅读
0
0

自建Docker Hub加速镜像

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

可选的加速方案

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

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

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

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

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

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

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

    • 优点…能用

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

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

自建加速镜像

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

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

方案一:使用 Nginx 搭建

系统:Ububtu 22.04 服务器:Zgocloud 洛杉矶 9929+CMIN2
提到要加速一个网站,自然就能想到使用 Nginx 反代一下了。接下来是具体的配置方案

  1. 安装 Nginx

    sudo apt update
    sudo apt install nginx
    
  2. 防火墙放行指定端口
    我这里使用的防火墙是系统自带的 UFW,并且没有开厂商提供的防火墙(忘记是关掉了还是本来就没有,反正没开),所以只需要 sudo ufw allow ‘Nginx Full’ 一条命令即可,这样就会放行 IPv4 和 IPv6 的 80 和 443 端口(当然也可以手动 sudo ufw allow 443 这样只开放 443 端口)
    如果没有使用防火墙,就不用设置这一步,如果还使用了厂商提供的防火墙,就需要在厂商的面板处同样开放这些端口

  3. 配置 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 / {
                         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;
    
                         # 对 upstream 状态码检查,实现 error_page 错误重定向
                         proxy_intercept_errors on;
                         # error_page 指令默认只检查了第一次后端返回的状态码,开启后可以跟随多次重定向。
                         recursive_error_pages on;
                         # 根据状态码执行对应操作,以下为301、302、307状态码都会触发
                         error_page 301 302 307 = @handle_redirect;
    
                 }
                 location @handle_redirect {
                         resolver 1.1.1.1;
                         set $saved_redirect_location '$upstream_http_location';
                         proxy_pass $saved_redirect_location;
                 }
         }
    

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

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

方案二:使用 CloudFlare Worker 搭建

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

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

  2. 接下来编辑代码,将 worker.js 的内容替换为下面内容

    worker.js

    import HTML from './docker.html';
    
    export default {
        async fetch(request) {
            const url = new URL(request.url);
            const path = url.pathname;
            const originalHost = request.headers.get("host");
            const registryHost = "registry-1.docker.io";
    
            if (path.startsWith("/v2/")) {
            const headers = new Headers(request.headers);
            headers.set("host", registryHost);
    
            const registryUrl = `https://${registryHost}${path}`;
            const registryRequest = new Request(registryUrl, {
                method: request.method,
                headers: headers,
                body: request.body,
                // redirect: "manual",
                redirect: "follow",
            });
    
            const registryResponse = await fetch(registryRequest);
    
            console.log(registryResponse.status);
    
            const responseHeaders = new Headers(registryResponse.headers);
            responseHeaders.set("access-control-allow-origin", originalHost);
            responseHeaders.set("access-control-allow-headers", "Authorization");
            return new Response(registryResponse.body, {
                status: registryResponse.status,
                statusText: registryResponse.statusText,
                headers: responseHeaders,
            });
            } else {
            return new Response(HTML.replace(/{{host}}/g, originalHost), {
                status: 200,
                headers: {
                "content-type": "text/html"
                }
            });
            }
        }
    }
    

    这里相比原项目,将 redirect: “manual” 修改为了 redirect: “follow”,目的是为了让脚本自行处理 307 跳转,直接返回给我们跳转后的数据。
    新建一个名为 docker.html 的 文件,内容如下

    docker.html

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="utf-8" />
            <meta name="viewport" content="width=device-width, initial-scale=1" />
            <title>Mirror Usage</title>
            <style>
            html {
            height: 100%;
            }
            body {
            font-family: "Roboto", "Helvetica", "Arial", sans-serif;
            font-size: 16px;
            color: #333;
            margin: 0;
            padding: 0;
            height: 100%;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
    
            }
            .container {
                margin: 0 auto;
                max-width: 600px;
            }
    
            .header {
                background-color: #438cf8;
                color: white;
                padding: 10px;
                display: flex;
                align-items: center;
            }
    
            h1 {
                font-size: 24px;
                margin: 0;
                padding: 0;
            }
    
            .content {
                padding: 32px;
            }
    
            .footer {
                background-color: #f2f2f2;
                padding: 10px;
                text-align: center;
                font-size: 14px;
            }
            </style>
        </head>
        <body>
            <div class="header">
            <h1>Mirror Usage</h1>
            </div>
            <div class="container">
            <div class="content">
                <p>镜像加速说明</p>
                <p>
                为了加速镜像拉取,你可以使用以下命令设置registery mirror:
                </p>
                <pre>
                sudo tee /etc/docker/daemon.json &lt;&lt;EOF
                {
                    "registry-mirrors": ["https://{{host}}"]
                }
                EOF
                </pre>
                </br>
                <p>
                为了避免 Worker 用量耗尽,你可以手动 pull 镜像然后 re-tag 之后 push 至本地镜像仓库:
                </p>
                <pre>
                docker pull {{host}}/library/alpine:latest # 拉取 library 镜像
                docker pull {{host}}/coredns/coredns:latest # 拉取 library 镜像
                </pre>
            </div>
            </div>
            <div class="footer">
            <p>Powered by Cloudflare Workers</p>
            </div>
        </body>
    </html>
    
  3. 接下来,点击右上角的 部署,稍等片刻

  4. 最后,返回面板,在 设置触发器 处设置一个自己的域名,一切就大功告成了
    不建议使用自带的 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 / {
                    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;

                    # 对 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;
            }
            # 处理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;
        }

}

新增的一些代码的作用:

  1. 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..人都麻了

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

  3. proxy_set_header Host 替换为在 CloudFlare Worker 设置的域名;
    将发给 CloudFlare Worker 的请求加上正确的域名方可让请求到达我们搭建的镜像源,如果不加会报 404

各种加速镜像实测

测试系统为 Debian12,测试镜像为 uptime-kuma: latest
使用命令 time docker pull 加速镜像地址/louislam/uptime-kuma 测试
测试时间为 2024 年 04 月 16 日 22 时

  1. dockerproxy.com Docker 镜像代理
    用时 19.370s

  2. docker.m.daocloud.io DaoCloud 镜像站
    用时 13.927s

  3. docker.mirrors.sjtug.sjtu.edu.cn 上海交大镜像站
    用时 12.157s

  4. docker.nju.edu.cn 南京大学镜像站
    用时 12.690s

  5. Nginx 反代自建方案

    1. 处理重定向

      • 晚上十点测试,用时 16.342s

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

    2. 不处理重定向
      次日下午五点测试,虽然不会出现使用官方源一样的卡死,但极慢

  6. CloudFlare Worker 反代自建方案
    几乎无法使用,部分层速度很慢

  7. 直连官方镜像仓库
    几乎无法使用,完全看脸

总结

根据测试,自建一个 Docker hub 加速镜像完全可行,使用 Nginx 搭建会比较看重服务器的线路,但只要不算太差,就完全可用了。不处理跳转会比较节省流量,但是为了速度,节省这一点流量也没什么必要。有一定的成本,但是如果本身就已经买了服务器的话,搭建个镜像源就几乎是顺带的事情了。
而使用 CloudFlare Worker 搭建..只能说稍微好一点点,如果服务器需要大量拉取镜像的话或许会用到这个方案。
而将方案一、二整合,几乎就是现阶段最完美的方案了,速度快且不受拉取次数的限制,只要 Docker hub 别乱变,就可以用超久。


参考文章


评论