Lainbo

Lainbo's Blog

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

How to Build Your Own Docker Hub Image

Since mid-May 2023, a mysterious force has rendered the Docker container platform https://hub.docker.com inaccessible.

More than a year has passed, and as of now (June 9, 2024), Nanjing University, University of Science and Technology of China, and Shanghai Jiao Tong University have clearly stopped their Docker image caching services. NetEase has already shut down, Tencent and Microsoft are reportedly available on the intranet, Alibaba can access subdomains after logging in, and Baidu seems to be down as well; dockerproxy has been blocked.

So, let's build one ourselves! (smiley face)

The two methods introduced in this article are not completely zero-threshold; Method One requires you to have your own domain name, and Method Two requires you to have your own overseas server.

Method One: Using Cloudflare Worker#

The registration method for Cloudflare will not be elaborated here; you can register with just an email. This method requires you to have your own domain name.

  1. Click on "Workers and Pages" in the menu bar, then click "Create Worker"
    image

  2. Name your Worker, for example, docker-proxy, click "Save," then click "Done."

    At this point, it should prompt you, "Congratulations! Your Worker has been deployed to the following region: Global"

  3. Click "Edit Code" on the right, delete the existing code on the left, and paste the following code. You need to change the workers_url at the top to the domain you want to deploy.

    • Code content
      'use strict'
      
      const hub_host = 'registry-1.docker.io'
      const auth_url = 'https://auth.docker.io'
      const workers_url = 'https://your-domain'
      //const home_page_url = 'remote-html-link'
      
      /** @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>Setting Up Docker Hub Registry Mirror on Linux</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>Setting Up Docker Hub Registry Mirror on Linux</h1>
          <p>To speed up the download speed of Docker images, you can set up a registry mirror for Docker Hub.</p>
      
          <h2>Setup Steps</h2>
          <ol>
            <li>Create or edit the <code>/etc/docker/daemon.json</code> file and add the following content:</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)">Copy</button>
          </div>
          <ol start="2">
            <li>Restart the Docker service:</li>
          </ol>
          <div class="code-wrapper">
            <pre>sudo systemctl restart docker</pre>
            <button class="copy-btn" onclick="copyCode(this)">Copy</button>
          </div>
      
          <p>After the setup is complete, Docker will pull images from the registry mirror you configured, speeding up the image download process.</p>
        </div>
      
        <script>
          function copyCode(btn) {
            const pre = btn.previousElementSibling;
            const code = pre.innerText;
            navigator.clipboard.writeText(code).then(function () {
              btn.innerText = 'Copied';
              setTimeout(function () {
                btn.innerText = 'Copy';
              }, 2000);
            }, function () {
              alert('Copy failed, please copy manually.');
            });
          }
        </script>
      </body>
      
      </html>
      `
      
  4. Click "Deploy" in the upper right corner.

  5. Since the domain name provided by the Worker is subject to DNS pollution, we need to return to the Worker, click in order as shown, enter the domain name you just filled in the code, and submit the changes. If your domain is hosted on Cloudflare, it should take effect in about 2 minutes; if hosted on other providers, you will need to CNAME it yourself.
    image

  6. At this point, we can enter the following command on the server to take effect:

    echo '{"registry-mirrors": ["https://your-domain"]}' | sudo tee /etc/docker/daemon.json > /dev/null
    
    sudo systemctl restart docker
    

Method Two: Using an Overseas Server to Build Your Own#

Lazy Script#

bash <(curl -sL https://raw.githubusercontent.com/lainbo/gists-hub/master/src/Linux/sh/deploy_registry.sh)

Or if you want to do it step by step#

  1. Create a docker-compose.yml file with the following content:

    #version: '3' # The latest version of Docker does not require this field
    services:
      registry:
        image: registry:2
        ports:
          - "17951:5000"
        environment:
          REGISTRY_PROXY_REMOTEURL: https://registry-1.docker.io  # Upstream source
          REGISTRY_STORAGE_CACHE_BLOBDESCRIPTOR: inmemory # In-memory cache
        volumes:
          - ./data:/var/lib/registry
    
  2. Run it with docker-compose up -d.


Whether using the lazy script or creating a docker-compose file, both will utilize Docker to start this self-built service on port 17951. Next, you need to:

  1. Reverse proxy the above-mentioned port 17951, assign a domain name, add an HTTPS certificate, and add a DNS record.

  2. At this point, we can enter the following command on the domestic server to take effect:

    echo '{"registry-mirrors": ["https://your-reverse-proxy-domain"]}' | sudo tee /etc/docker/daemon.json > /dev/null
    
    sudo systemctl restart docker
    

Method Three: Using huecker#

This is a ready-made mirror set up by an author.

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

And some other overseas mirrors worth trying:

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.