Lainbo

Lainbo's Blog

If you’re nothing without the suit, then you shouldn't have it.
github
email
follow

自建Docker Hub映像方法

自從 2023 年 5 月中旬,因為一股神秘力量導致 Docker 容器平台 https://hub.docker.com 無法訪問了。

一年多過去了,截止目前(2024 年 6 月 9 日),南京大學、中科大、上海交大 目前明確停止 docker 鏡像緩存服務。網易之前就死了,騰訊微軟據說內網可用,阿里登錄後就可以拿到子域名,百度好像也掛了,dockerproxy 被牆。

所以,讓我們來自建一個吧!(微笑臉)

本文介紹的兩種方式並不是完全的零門檻,方式一需要你有自己的域名,方式二需要你有自己的境外伺服器。

方式一:使用 Cloudflare Worker#

註冊Cloudflare的方法不再贅述,一個郵箱就能註冊,這個方式需要你有一個自己的域名

  1. 點擊菜單欄的「Worker 和 Pages」,然後點擊創建 Worker
    image

  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

  6. 此時我們在伺服器中輸入以下命令即可生效

    echo '{"registry-mirrors": ["https://你的域名"]}' | sudo tee /etc/docker/daemon.json > /dev/null
    
    sudo systemctl restart 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
    

方式三:使用huecker#

就是一個作者搭建的現成鏡像

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

和一些其他值得嘗試的境外鏡像

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。