Site icon image Lainbo's Blog

一个自言自语的地方,希望能帮到你

自建Docker Hub镜像方法

自从2023年5月中旬,因为一股神秘力量导致Docker 容器平台 https://hub.docker.com 无法访问了。

一年多过去了,截止目前(2024年6月9日),南京大学、中科大、上海交大 目前明确停止docker镜像缓存服务。网易之前就死了,腾讯微软据说内网可用,阿里登陆后就可以拿到子域名,百度好像也挂了,dockerproxy被墙。

所以,让我们来自建一个吧!(微笑脸)

本文介绍的两种方式并不是完全的零门槛,方式一需要你有自己的域名,方式二需要你有自己的境外服务器。

方式一:使用Cloudflare Worker

注册Cloudflare的方法不再赘述,一个邮箱就能注册,这个方式需要你有一个自己的域名

  1. 点击菜单栏的「Worker和Pages」,然后点击创建Worker
    Image in a image block
  2. 给你的Worker起个名字,比如docker-proxy,点击「保存」,之后点击「完成」

    此时他应该会提示你“恭喜!您的 Worker 已部署到以下区域:全球

  3. 点击右侧的「编辑代码」,将左侧已有代码删除,然后粘贴以下代码。需要将顶部的workers_url 修改为你要部署的域名
    代码内容
    'use strict'
    
    const hub_host = 'registry-1.docker.io'
    const auth_url = 'https://auth.docker.io'
    const workers_url = 'https://你的域名'
    //const home_page_url = '远程html链接'
    
    /** @type {RequestInit} */
    const PREFLIGHT_INIT = {
        status: 204,
        headers: new Headers({
            'access-control-allow-origin': '*',
            'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS',
            '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})
    }
    
    
    /**
     * @param {string} urlStr
     */
    function newUrl(urlStr) {
        try {
            return new URL(urlStr)
        } catch (err) {
            return null
        }
    }
    
    
    addEventListener('fetch', e => {
        const ret = fetchHandler(e)
            .catch(err => makeRes('cfworker error:\n' + err.stack, 502))
        e.respondWith(ret)
    })
    
    
    /**
     * @param {FetchEvent} e
     */
    async function fetchHandler(e) {
        const getReqHeader = (key) => e.request.headers.get(key);
    
        let url = new URL(e.request.url);
    
        if (url.pathname === '/') {
            // Fetch and return the home page HTML content
            //return fetch(home_page_url);
            return new Response(indexHtml, {
                headers: {
                  'Content-Type': 'text/html'
                }
              });
        }
    
        if (url.pathname === '/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, e.request), token_parameter)
        }
    
        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
        };
    
        if (e.request.headers.has("Authorization")) {
            parameter.headers.Authorization = getReqHeader("Authorization");
        }
    
        let original_response = await fetch(new Request(url, e.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;
    
        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(e.request, new_response_headers.get("Location"))
        }
    
        let response = new Response(original_text, {
            status,
            headers: new_response_headers
        })
        return response;
    
    }
    
    
    /**
     * @param {Request} req
     * @param {string} pathname
     */
    function httpHandler(req, pathname) {
        const reqHdrRaw = req.headers
    
        // preflight
        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, 0)
    }
    
    
    /**
     *
     * @param {URL} urlObj
     * @param {RequestInit} reqInit
     */
    async function proxy(urlObj, reqInit, rawLen) {
        const res = await fetch(urlObj.href, reqInit)
        const resHdrOld = res.headers
        const resHdrNew = new Headers(resHdrOld)
    
        // verify
        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
        })
    }
    
    const indexHtml = `
    <!DOCTYPE html>
    <html lang="zh-CN">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>在Linux上设置Docker Hub Registry Mirror</title>
      <style>
        body {
          max-width: 800px;
          margin: 0 auto;
          padding: 20px;
          line-height: 1.6;
          color: #333;
          background-color: #f7f7f7;
        }
    
        h1 {
          color: #0066cc;
          margin-bottom: 30px;
        }
    
        h2 {
          color: #0066cc;
          margin-top: 40px;
        }
    
        pre {
          background-color: #fff;
          padding: 15px;
          padding-top: 48px;
          overflow-x: auto;
          border-radius: 8px;
          box-shadow: 0 4px 14px rgba(0, 0, 0, 0.1);
        }
    
        ol {
          margin-top: 20px;
          padding-left: 20px;
        }
    
        li {
          margin-bottom: 10px;
        }
    
        code {
          font-family: Consolas, monospace;
          background-color: #fff;
          padding: 2px 4px;
          border-radius: 3px;
        }
    
        .container {
          background-color: #fff;
          padding: 30px;
          border-radius: 8px;
          box-shadow: 0 4px 14px rgba(0, 0, 0, 0.1);
        }
    
        .copy-btn {
          position: absolute;
          top: 10px;
          right: 10px;
          padding: 4px 10px;
          background-color: #0066cc;
          color: #fff;
          border: none;
          border-radius: 4px;
          cursor: pointer;
          font-size: 14px;
          transition: background-color 0.3s;
        }
    
        .copy-btn:hover {
          background-color: #0052a3af;
        }
    
        .code-wrapper {
          position: relative;
        }
      </style>
    </head>
    
    <body>
      <div class="container">
        <h1>在Linux上设置Docker Hub Registry Mirror</h1>
        <p>为了加速Docker镜像的下载速度,你可以设置Docker Hub的registry mirror</p>
    
        <h2>设置步骤</h2>
        <ol>
          <li>创建或编辑<code>/etc/docker/daemon.json</code>文件,添加以下内容:</li>
        </ol>
        <div class="code-wrapper">
          <pre>
    echo '{"registry-mirrors": ["${workers_url}"]}' | sudo tee /etc/docker/daemon.json > /dev/null</pre>
          <button class="copy-btn" onclick="copyCode(this)">复制</button>
        </div>
        <ol start="2">
          <li>重启Docker服务:</li>
        </ol>
        <div class="code-wrapper">
          <pre>sudo systemctl restart docker</pre>
          <button class="copy-btn" onclick="copyCode(this)">复制</button>
        </div>
    
        <p>设置完成后,Docker将会从您配置的registry mirror中拉取镜像,加速镜像下载过程。</p>
      </div>
    
      <script>
        function copyCode(btn) {
          const pre = btn.previousElementSibling;
          const code = pre.innerText;
          navigator.clipboard.writeText(code).then(function () {
            btn.innerText = '已复制';
            setTimeout(function () {
              btn.innerText = '复制';
            }, 2000);
          }, function () {
            alert('复制失败,请手动复制。');
          });
        }
      </script>
    </body>
    
    </html>
    `
  4. 点击右上角「部署」
  5. 因为Worker提供的域名是被DNS污染的,所以我们需要回到Worker,如图依次点击,输入你刚刚在代码里填写的域名后提交更改,如果你的域名托管在Cloudflare,则只需要等2分钟左右即可生效;如果在其他的服务商托管,你需要自己去CNAME一下
    Image in a image block
  6. 此时我们在服务器中输入以下命令即可生效
    echo '{"registry-mirrors": ["https://你的域名"]}' | sudo tee /etc/docker/daemon.json > /dev/null
    设置镜像
    sudo systemctl restart docker
    重启Docker让设置生效

方式二:使用境外服务器自建

懒人脚本
bash <(curl -sL https://raw.githubusercontent.com/lainbo/gists-hub/master/src/Linux/sh/deploy_registry.sh)
或者你想自己一步一步来
  1. 创建一个docker-compose.yml文件,内容如下
    #version: '3' #最新版本docker 不需要此字段
    services:
      registry:
        image: registry:2
        ports:
          - "17951:5000"
        environment:
          REGISTRY_PROXY_REMOTEURL: https://registry-1.docker.io  # 上游源
          REGISTRY_STORAGE_CACHE_BLOBDESCRIPTOR: inmemory # 内存缓存
        volumes:
          - ./data:/var/lib/registry
  2. 运行起来,docker-compose up -d

以上不论是懒人脚本,还是自建创建docker-compose文件,都会利用docker在17951端口上起这个自建服务,接下来就是

  1. 反代一下上面写的17951端口,给个域名,https证书加一下,DNS解析添一条
  2. 此时我们在境内服务器中输入以下命令即可生效
    echo '{"registry-mirrors": ["https://你反代的域名"]}' | sudo tee /etc/docker/daemon.json > /dev/null
    设置镜像
    sudo systemctl restart docker
    重启Docker让设置生效

方式三:使用huecker

就是一个作者搭建的现成镜像

{
  "registry-mirrors": ["https://huecker.io"]
}

和一些其他值得尝试的境外镜像