提要

因为国内目前无法正常使用官方的Docker Hub拉取镜像,特别是经常要在服务器上使用,

所需要素

  • 域名:最好自有一个域名,且该域名需要托管在Cloudflare上,否则只能使用随机域名访问,不太方便

Workers部署

登录Cloudflare之后,打开Workers,创建一个新的Worker,在worker.js中输入如下内容,并将workers_url的内容替换成自己的域名,例如(https://hub.domain.com)。然后点击部署

这个已失效

'use strict'

const hub_host = 'registry-1.docker.io'
const auth_url = 'https://auth.docker.io'
const workers_url = '<Your_domain_name(https://hub.domain.com)>'
/**
 * static files (404.html, sw.js, conf.js)
 */

/** @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 === '/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
    })
}

部署完之后,在该worker的“设置”选项中找到“触发器”,点击"添加自定义域"。将js中workers_url定义的域名添加进去,等待几分钟之后域名解析生效。

新的可用版本 修改第328行的域名即可

addEventListener("fetch", (event) => {
  event.passThroughOnException();
  event.respondWith(handleRequest(event.request));
});

const dockerHub = "https://registry-1.docker.io";
const HTML = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Docker 镜像代理使用说明</title>
    <style>
        body {
            font-family: 'Roboto', sans-serif;
            margin: 0;
            padding: 0;
            background-color: #f4f4f4;
        }
        .header {
            background: linear-gradient(135deg, #667eea, #764ba2);
            color: #fff;
            padding: 20px 0;
            text-align: center;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
        }
        .container {
            max-width: 800px;
            margin: 40px auto;
            padding: 20px;
            background-color: #fff;
            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
            border-radius: 10px;
        }
        .content {
            margin-bottom: 20px;
        }
        .footer {
            text-align: center;
            padding: 20px 0;
            background-color: #333;
            color: #fff;
        }
        pre {
            background-color: #272822;
            color: #f8f8f2;
            padding: 15px;
            border-radius: 5px;
            overflow-x: auto;
        }
        code {
            font-family: 'Source Code Pro', monospace;
        }
        a {
            font-weight: bold;
            color: #ffffff;
            text-decoration: none;
        }
        a:hover {
            text-decoration: underline;
        }
        @media (max-width: 600px) {
            .container {
                margin: 20px;
                padding: 15px;
            }
            .header {
                padding: 15px 0;
            }
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>Docker 镜像代理使用说明</h1>
    </div>
    <div class="container">
        <div class="content">
          <h3>带镜像仓库地址使用说明</h3>
          <p>1.拉取镜像</p>
          <pre><code># 拉取 redis 官方镜像(不带命名空间)
docker pull {:host}/redis

# 拉取 rabbitmq 官方镜像
docker pull {:host}/library/rabbitmq

# 拉取 postgresql 非官方镜像
docker pull {:host}/bitnami/postgresql</code></pre><p>2.重命名镜像</p>
          <pre><code># 重命名 redis 镜像
docker tag {:host}/library/redis redis 

# 重命名 postgresql 镜像
docker tag {:host}/bitnami/postgresql bitnami/postgresql</code></pre><h3>镜像源方式使用说明</h3><p>1.添加镜像源</p>
          <pre><code># 添加镜像代理到 Docker 镜像源
sudo tee /etc/docker/daemon.json &lt;&lt; EOF
{
  "registry-mirrors": ["https://{:host}"]
}
EOF</code></pre><p>2.拉取镜像</p>
<pre><code># 拉取 redis 官方镜像
docker pull redis

# 拉取 rabbitmq 非官方镜像
docker pull bitnami/rabbitmq

# 拉取 postgresql 官方镜像
docker pull postgresql</code></pre>
        </div>
    </div>
    <div class="footer">
        <p>All rights reserved. Powered by <a href="https://cloudflare.com">Cloudflare</a>.</p>
    </div>
</body>
</html>
`

const routes = {
  // 替换为你的域名
  "hub1.kaside365.com": dockerHub,
};

function routeByHosts(host) {
  if (host in routes) {
    return routes[host];
  }
  return "";
}

async function handleRequest(request) {

  const url = new URL(request.url);

  if (url.pathname == "/") {
    return handleHomeRequest(url.host);
  }

  const upstream = routeByHosts(url.hostname);
  if (!upstream) {
    return createNotFoundResponse(routes);
  }

  const isDockerHub = upstream == dockerHub;
  const authorization = request.headers.get("Authorization");
  if (url.pathname == "/v2/") {
    return handleFirstRequest(upstream, authorization, url.hostname);
  }
  // get token
  if (url.pathname == "/v2/auth") {
    return handleAuthRequest(upstream, url, isDockerHub, authorization);
  }
  // redirect for DockerHub library images
  // Example: /v2/busybox/manifests/latest => /v2/library/busybox/manifests/latest
  if (isDockerHub) {
    const pathParts = url.pathname.split("/");
    if (pathParts.length == 5) {
      pathParts.splice(2, 0, "library");
      const redirectUrl = new URL(url);
      redirectUrl.pathname = pathParts.join("/");
      return Response.redirect(redirectUrl.toString(), 301);
    }
  }
  return handlePullRequest(upstream, request);
}

function parseAuthenticate(authenticateStr) {
  // sample: Bearer realm="https://auth.ipv6.docker.com/token",service="registry.docker.io"
  // match strings after =" and before "
  const re = /(?<=\=")(?:\\.|[^"\\])*(?=")/g;
  const matches = authenticateStr.match(re);
  if (matches == null || matches.length < 2) {
    throw new Error(`invalid Www-Authenticate Header: ${authenticateStr}`);
  }
  return {
    realm: matches[0],
    service: matches[1],
  };
}

async function fetchToken(wwwAuthenticate, scope, authorization) {
  const url = new URL(wwwAuthenticate.realm);
  if (wwwAuthenticate.service.length) {
    url.searchParams.set("service", wwwAuthenticate.service);
  }
  if (scope) {
    url.searchParams.set("scope", scope);
  }
  const headers = new Headers();
  if (authorization) {
    headers.set("Authorization", authorization);
  }
  return await fetch(url, { method: "GET", headers: headers });
}

function handleHomeRequest(host) {
  return new Response(HTML.replace(/{:host}/g, host), {
    status: 200,
    headers: {
      "content-type": "text/html",
    }
  })
}

async function handlePullRequest(upstream, request) {
  const url = new URL(request.url);
  const newUrl = new URL(upstream + url.pathname);
  const newReq = new Request(newUrl, {
    method: request.method,
    headers: request.headers,
    redirect: "follow",
  });
  return await fetch(newReq);
}

async function handleFirstRequest(upstream, authorization, hostname) {
  const newUrl = new URL(upstream + "/v2/");
  const headers = new Headers();
  if (authorization) {
    headers.set("Authorization", authorization);
  }
  // check if need to authenticate
  const resp = await fetch(newUrl.toString(), {
    method: "GET",
    headers: headers,
    redirect: "follow",
  });
  if (resp.status === 401) {
      headers.set(
        "Www-Authenticate",
        `Bearer realm="https://${hostname}/v2/auth",service="cloudflare-docker-proxy"`
      );
    return new Response(JSON.stringify({ message: "Unauthorized" }), {
      status: 401,
      headers: headers,
    });
  } else {
    return resp;
  }
}

async function handleAuthRequest(upstream, url, isDockerHub, authorization) {
  const newUrl = new URL(upstream + "/v2/");
  const resp = await fetch(newUrl.toString(), {
    method: "GET",
    redirect: "follow",
  });
  if (resp.status !== 401) {
    return resp;
  }
  const authenticateStr = resp.headers.get("WWW-Authenticate");
  if (authenticateStr === null) {
    return resp;
  }
  const wwwAuthenticate = parseAuthenticate(authenticateStr);
  let scope = url.searchParams.get("scope");
  // autocomplete repo part into scope for DockerHub library images
  // Example: repository:busybox:pull => repository:library/busybox:pull
  if (scope && isDockerHub) {
    let scopeParts = scope.split(":");
    if (scopeParts.length == 3 && !scopeParts[1].includes("/")) {
      scopeParts[1] = "library/" + scopeParts[1];
      scope = scopeParts.join(":");
    }
  }
  return await fetchToken(wwwAuthenticate, scope, authorization);
}

const createNotFoundResponse = (routes) => new Response(
  JSON.stringify({ routes }),
  {
    status: 404,
    headers: {
      "Content-Type": "application/json",
    },
  }
);

拉取规则

在直接使用官方Docker Hub拉取镜像时,一般的格式有以下两种

docker pull nginx
docker pull gitlab/gitlab-ce

第一种是官方镜像,全部的结构格式为docker.io/library/nginx。在docker hub上搜索时就能看到Official Images字样。

替换之后则是hub.domain.com/library/nginx

docker pull hub.domain.com/library/nginx:latest

第二种是社区镜像,全部的结构格式为docker.io/gitlab/gitlab-ce 替换之后的则是hub.domain.com/gitlab/gitlab-ce

docker pull hub.domain.com/gitlab/gitlab-ce:latest

最后更新于