提要
因为国内目前无法正常使用官方的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 << 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最后更新于