自從 2023 年 5 月中旬,因為一股神秘力量導致 Docker 容器平台 https://hub.docker.com 無法訪問了。
一年多過去了,截止目前(2024 年 6 月 9 日),南京大學、中科大、上海交大 目前明確停止 docker 鏡像緩存服務。網易之前就死了,騰訊微軟據說內網可用,阿里登錄後就可以拿到子域名,百度好像也掛了,dockerproxy 被牆。
所以,讓我們來自建一個吧!(微笑臉)
本文介紹的兩種方式並不是完全的零門檻,方式一需要你有自己的域名,方式二需要你有自己的境外伺服器。
方式一:使用 Cloudflare Worker#
註冊Cloudflare的方法不再贅述,一個郵箱就能註冊,這個方式需要你有一個自己的域名
-
點擊菜單欄的「Worker 和 Pages」,然後點擊創建 Worker
-
給你的 Worker 起個名字,比如 docker-proxy,點擊「保存」,之後點擊「完成」
此時他應該會提示你 “恭喜!您的 Worker 已部署到以下區域:全球”
-
點擊右側的「編輯代碼」,將左側已有代碼刪除,然後粘貼以下代碼。需要將頂部的
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> `
- 代碼內容
-
點擊右上角「部署」
-
因為 Worker 提供的域名是被 DNS 污染的,所以我們需要回到 Worker,如圖依次點擊,輸入你剛剛在代碼裡填寫的域名後提交更改,如果你的域名托管在 Cloudflare,則只需要等 2 分鐘左右即可生效;如果在其他的服務商托管,你需要自己去 CNAME 一下
-
此時我們在伺服器中輸入以下命令即可生效
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)
或者你想自己一步一步來#
-
創建一個 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
-
運行起來,
docker-compose up -d
以上不論是懶人腳本,還是自建創建 docker-compose 文件,都会利用 docker 在 17951 端口上起這個自建服務,接下來就是
-
反代一下上面寫的 17951 端口,給個域名,https 證書加一下,DNS 解析添一條
-
此時我們在境內伺服器中輸入以下命令即可生效
echo '{"registry-mirrors": ["https://你反代的域名"]}' | sudo tee /etc/docker/daemon.json > /dev/null
sudo systemctl restart docker
方式三:使用huecker#
就是一個作者搭建的現成鏡像
{
"registry-mirrors": ["https://huecker.io"]
}
和一些其他值得嘗試的境外鏡像
鏡像地址 | 描述 |
---|---|
https://public.ecr.aws | Amazon |
https://dockerhub.timeweb.cloud | Timeweb |
https://cr.yandex/mirror | Yandex |
https://dh-mirror.gitverse.ru | GitVerse |
https://dockerhub1.beget.com | Beget |
https://noohub.ru | NooSoft |
https://jockerhub.com | DpkgSoft |
https://registry.access.redhat.com | Red Hat |
https://registry.redhat.io | Red Hat |