自建Docker镜像加速

自建Docker镜像加速

前言

由于最近各大院校 Docker 镜像要么关闭要么限制IP访问,因此家中设备部署容器不方便,可以通过自建镜像来加速拉取镜像。

Tip

代码来自下面的博文,转载记录。

自建 Docker Hub 加速镜像

首先推荐一个网站:

部署

Nginx 代理

配置三个地址:

  • 官方仓库地址: registry-1.docker.io
  • jwt 授权地址: auth.docker.io
  • api 地址: index.docker.io
 1# 使用 map 来匹配和替换 upstream 头部中的 auth.docker.io
 2map $upstream_http_www_authenticate $m_www_authenticate_replaced {
 3    "~auth\.docker\.io(.*)" "$1";
 4    default "";
 5}
 6
 7map $m_www_authenticate_replaced $m_final_replaced {
 8    "~(.*)" 'Bearer realm=\"$scheme://$host$1';
 9    default "";
10}
11
12server
13    {
14        listen 443 ssl http2;
15        # 改成自己的域名
16        server_name xxxx.example.com;
17
18        # 证书部分
19        ssl_certificate 证书地址;
20        ssl_certificate_key 密钥地址;
21
22        ssl_session_timeout 24h;
23
24        # TLS 版本控制
25        ssl_protocols TLSv1.2 TLSv1.3;
26        ssl_prefer_server_ciphers on;      
27        ssl_ciphers TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-256-GCM-SHA384:TLS13-AES-128-GCM-SHA256:EECDH+CHACHA20:EECDH+AESGCM:EECDH+AES;
28
29        proxy_ssl_server_name on;
30
31        proxy_set_header X-Real-IP $remote_addr;
32        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
33        proxy_set_header X-Forwarded-Proto $scheme;
34
35        # 修改jwt授权地址
36        proxy_hide_header www-authenticate;
37        add_header www-authenticate "$m_final_replaced" always;
38
39        # 关闭缓存
40        proxy_buffering off;
41        # 转发认证相关
42        proxy_set_header Authorization $http_authorization;
43        proxy_pass_header  Authorization;
44
45        # 对 upstream 状态码检查,实现 error_page 错误重定向
46        proxy_intercept_errors on;
47        recursive_error_pages on;
48        # 根据状态码执行对应操作,以下为301、302、307状态码都会触发
49        error_page 301 302 307 = @handle_redirect;
50
51        # v1 api
52        location /v1 {
53            proxy_pass https://index.docker.io;
54            proxy_set_header Host index.docker.io;
55        }
56
57        # v2 api
58        location /v2 {
59            proxy_pass https://index.docker.io;
60            proxy_set_header Host index.docker.io;
61        }
62
63        # jwt授权地址
64        location /token {
65            proxy_pass https://auth.docker.io;
66            proxy_set_header Host auth.docker.io;
67        }
68
69        location / {
70            # Docker hub 的官方镜像仓库
71            proxy_pass https://registry-1.docker.io;
72            proxy_set_header Host registry-1.docker.io;
73        }
74        
75        #处理重定向
76        location @handle_redirect {
77            resolver 1.1.1.1;
78            set $saved_redirect_location '$upstream_http_location';
79            proxy_pass $saved_redirect_location;
80        }
81    }

Cloudflare Workers

  1'use strict'
  2
  3const hub_host = 'registry-1.docker.io'
  4const auth_url = 'https://auth.docker.io'
  5const workers_url = 'https://docker.xxxxx.workers.dev' // 换成实际的worker地址,或者绑定的自定义域名
  6/**
  7 * static files (404.html, sw.js, conf.js)
  8 */
  9
 10/** @type {RequestInit} */
 11const PREFLIGHT_INIT = {
 12    status: 204,
 13    headers: new Headers({
 14        'access-control-allow-origin': '*',
 15        'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS',
 16        'access-control-max-age': '1728000',
 17    }),
 18}
 19
 20/**
 21 * @param {any} body
 22 * @param {number} status
 23 * @param {Object<string, string>} headers
 24 */
 25function makeRes(body, status = 200, headers = {}) {
 26    headers['access-control-allow-origin'] = '*'
 27    return new Response(body, {status, headers})
 28}
 29
 30
 31/**
 32 * @param {string} urlStr
 33 */
 34function newUrl(urlStr) {
 35    try {
 36        return new URL(urlStr)
 37    } catch (err) {
 38        return null
 39    }
 40}
 41
 42
 43addEventListener('fetch', e => {
 44    const ret = fetchHandler(e)
 45        .catch(err => makeRes('cfworker error:\n' + err.stack, 502))
 46    e.respondWith(ret)
 47})
 48
 49
 50/**
 51 * @param {FetchEvent} e
 52 */
 53async function fetchHandler(e) {
 54  const getReqHeader = (key) => e.request.headers.get(key);
 55
 56  let url = new URL(e.request.url);
 57
 58  if (url.pathname === '/token') {
 59      let token_parameter = {
 60        headers: {
 61        'Host': 'auth.docker.io',
 62        'User-Agent': getReqHeader("User-Agent"),
 63        'Accept': getReqHeader("Accept"),
 64        'Accept-Language': getReqHeader("Accept-Language"),
 65        'Accept-Encoding': getReqHeader("Accept-Encoding"),
 66        'Connection': 'keep-alive',
 67        'Cache-Control': 'max-age=0'
 68        }
 69      };
 70      let token_url = auth_url + url.pathname + url.search
 71      return fetch(new Request(token_url, e.request), token_parameter)
 72  }
 73
 74  url.hostname = hub_host;
 75  
 76  let parameter = {
 77    headers: {
 78      'Host': hub_host,
 79      'User-Agent': getReqHeader("User-Agent"),
 80      'Accept': getReqHeader("Accept"),
 81      'Accept-Language': getReqHeader("Accept-Language"),
 82      'Accept-Encoding': getReqHeader("Accept-Encoding"),
 83      'Connection': 'keep-alive',
 84      'Cache-Control': 'max-age=0'
 85    },
 86    cacheTtl: 3600
 87  };
 88
 89  if (e.request.headers.has("Authorization")) {
 90    parameter.headers.Authorization = getReqHeader("Authorization");
 91  }
 92
 93  let original_response = await fetch(new Request(url, e.request), parameter)
 94  let original_response_clone = original_response.clone();
 95  let original_text = original_response_clone.body;
 96  let response_headers = original_response.headers;
 97  let new_response_headers = new Headers(response_headers);
 98  let status = original_response.status;
 99
100  if (new_response_headers.get("WWW-Authenticate")) {
101    let re = new RegExp(auth_url, 'g');
102    new_response_headers.set("WWW-Authenticate", response_headers.get("WWW-Authenticate").replace(re, workers_url));
103  }
104
105  if (new_response_headers.get("Location")) {
106    return httpHandler(e.request, new_response_headers.get("Location"))
107  }
108
109  let response = new Response(original_text, {
110            status,
111            headers: new_response_headers
112        })
113  return response;
114  
115}
116
117
118/**
119 * @param {Request} req
120 * @param {string} pathname
121 */
122function httpHandler(req, pathname) {
123    const reqHdrRaw = req.headers
124
125    // preflight
126    if (req.method === 'OPTIONS' &&
127        reqHdrRaw.has('access-control-request-headers')
128    ) {
129        return new Response(null, PREFLIGHT_INIT)
130    }
131
132    let rawLen = ''
133
134    const reqHdrNew = new Headers(reqHdrRaw)
135
136    const refer = reqHdrNew.get('referer')
137
138    let urlStr = pathname
139    
140    const urlObj = newUrl(urlStr)
141
142    /** @type {RequestInit} */
143    const reqInit = {
144        method: req.method,
145        headers: reqHdrNew,
146        redirect: 'follow',
147        body: req.body
148    }
149    return proxy(urlObj, reqInit, rawLen, 0)
150}
151
152
153/**
154 *
155 * @param {URL} urlObj
156 * @param {RequestInit} reqInit
157 */
158async function proxy(urlObj, reqInit, rawLen) {
159    const res = await fetch(urlObj.href, reqInit)
160    const resHdrOld = res.headers
161    const resHdrNew = new Headers(resHdrOld)
162
163    // verify
164    if (rawLen) {
165        const newLen = resHdrOld.get('content-length') || ''
166        const badLen = (rawLen !== newLen)
167
168        if (badLen) {
169            return makeRes(res.body, 400, {
170                '--error': `bad len: ${newLen}, except: ${rawLen}`,
171                'access-control-expose-headers': '--error',
172            })
173        }
174    }
175    const status = res.status
176    resHdrNew.set('access-control-expose-headers', '*')
177    resHdrNew.set('access-control-allow-origin', '*')
178    resHdrNew.set('Cache-Control', 'max-age=1500')
179    
180    resHdrNew.delete('content-security-policy')
181    resHdrNew.delete('content-security-policy-report-only')
182    resHdrNew.delete('clear-site-data')
183
184    return new Response(res.body, {
185        status,
186        headers: resHdrNew
187    })
188}

整合方案

当超出请求数量限制,返回 429 错误时,将后端转发给 CloudFlare Workers

 1# 使用 map 来匹配和替换 upstream 头部中的 auth.docker.io
 2map $upstream_http_www_authenticate $m_www_authenticate_replaced {
 3    "~auth\.docker\.io(.*)" "$1";
 4    default "";
 5}
 6
 7map $m_www_authenticate_replaced $m_final_replaced {
 8    "~(.*)" 'Bearer realm=\"$scheme://$host$1';
 9    default "";
10}
11
12server
13    {
14        listen 443 ssl http2;
15        # 改成自己的域名
16        server_name xxxx.example.com;
17
18        # 证书部分
19        ssl_certificate 证书地址;
20        ssl_certificate_key 密钥地址;
21
22        ssl_session_timeout 24h;
23
24        # TLS 版本控制
25        ssl_protocols TLSv1.2 TLSv1.3;
26        ssl_prefer_server_ciphers on;      
27        ssl_ciphers TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-256-GCM-SHA384:TLS13-AES-128-GCM-SHA256:EECDH+CHACHA20:EECDH+AESGCM:EECDH+AES;
28
29        proxy_ssl_server_name on;
30
31        proxy_set_header X-Real-IP $remote_addr;
32        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
33        proxy_set_header X-Forwarded-Proto $scheme;
34
35        # 修改jwt授权地址
36        proxy_hide_header www-authenticate;
37        add_header www-authenticate "$m_final_replaced" always;
38
39        # 关闭缓存
40        proxy_buffering off;
41        # 转发认证相关
42        proxy_set_header Authorization $http_authorization;
43        proxy_pass_header  Authorization;
44
45        # 对 upstream 状态码检查,实现 error_page 错误重定向
46        proxy_intercept_errors on;
47        recursive_error_pages on;
48        # 根据状态码执行对应操作,以下为301、302、307状态码都会触发
49        error_page 301 302 307 = @handle_redirect;
50
51        error_page 429 = @handle_too_many_requests;
52
53        # v1 api
54        location /v1 {
55            proxy_pass https://index.docker.io;
56            proxy_set_header Host index.docker.io;
57        }
58
59        # v2 api
60        location /v2 {
61            proxy_pass https://index.docker.io;
62            proxy_set_header Host index.docker.io;
63        }
64
65        # jwt授权地址
66        location /token {
67            proxy_pass https://auth.docker.io;
68            proxy_set_header Host auth.docker.io;
69        }
70
71        location / {
72            # Docker hub 的官方镜像仓库
73            proxy_pass https://registry-1.docker.io;
74            proxy_set_header Host registry-1.docker.io;
75        }
76        
77        #处理重定向
78        location @handle_redirect {
79            resolver 1.1.1.1;
80            set $saved_redirect_location '$upstream_http_location';
81            proxy_pass $saved_redirect_location;
82        }
83
84        # 处理429错误
85        location @handle_too_many_requests {
86            proxy_set_header Host docker.xxxxx.workers.dev;  # 替换为另一个服务器的地址
87            proxy_pass https://docker.xxxxx.workers.dev;
88        }
89    }
  1'use strict'
  2
  3const hub_host = 'registry-1.docker.io'
  4const auth_url = 'https://auth.docker.io'
  5const workers_url = 'https://xxxx.example.com' // 改为nginx代理的地址
  6/**
  7 * static files (404.html, sw.js, conf.js)
  8 */
  9
 10/** @type {RequestInit} */
 11const PREFLIGHT_INIT = {
 12    status: 204,
 13    headers: new Headers({
 14        'access-control-allow-origin': '*',
 15        'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS',
 16        'access-control-max-age': '1728000',
 17    }),
 18}
 19
 20/**
 21 * @param {any} body
 22 * @param {number} status
 23 * @param {Object<string, string>} headers
 24 */
 25function makeRes(body, status = 200, headers = {}) {
 26    headers['access-control-allow-origin'] = '*'
 27    return new Response(body, {status, headers})
 28}
 29
 30
 31/**
 32 * @param {string} urlStr
 33 */
 34function newUrl(urlStr) {
 35    try {
 36        return new URL(urlStr)
 37    } catch (err) {
 38        return null
 39    }
 40}
 41
 42
 43addEventListener('fetch', e => {
 44    const ret = fetchHandler(e)
 45        .catch(err => makeRes('cfworker error:\n' + err.stack, 502))
 46    e.respondWith(ret)
 47})
 48
 49
 50/**
 51 * @param {FetchEvent} e
 52 */
 53async function fetchHandler(e) {
 54  const getReqHeader = (key) => e.request.headers.get(key);
 55
 56  let url = new URL(e.request.url);
 57
 58  if (url.pathname === '/token') {
 59      let token_parameter = {
 60        headers: {
 61        'Host': 'auth.docker.io',
 62        'User-Agent': getReqHeader("User-Agent"),
 63        'Accept': getReqHeader("Accept"),
 64        'Accept-Language': getReqHeader("Accept-Language"),
 65        'Accept-Encoding': getReqHeader("Accept-Encoding"),
 66        'Connection': 'keep-alive',
 67        'Cache-Control': 'max-age=0'
 68        }
 69      };
 70      let token_url = auth_url + url.pathname + url.search
 71      return fetch(new Request(token_url, e.request), token_parameter)
 72  }
 73
 74  url.hostname = hub_host;
 75  
 76  let parameter = {
 77    headers: {
 78      'Host': hub_host,
 79      'User-Agent': getReqHeader("User-Agent"),
 80      'Accept': getReqHeader("Accept"),
 81      'Accept-Language': getReqHeader("Accept-Language"),
 82      'Accept-Encoding': getReqHeader("Accept-Encoding"),
 83      'Connection': 'keep-alive',
 84      'Cache-Control': 'max-age=0'
 85    },
 86    cacheTtl: 3600
 87  };
 88
 89  if (e.request.headers.has("Authorization")) {
 90    parameter.headers.Authorization = getReqHeader("Authorization");
 91  }
 92
 93  let original_response = await fetch(new Request(url, e.request), parameter)
 94  let original_response_clone = original_response.clone();
 95  let original_text = original_response_clone.body;
 96  let response_headers = original_response.headers;
 97  let new_response_headers = new Headers(response_headers);
 98  let status = original_response.status;
 99
100  if (new_response_headers.get("WWW-Authenticate")) {
101    let re = new RegExp(auth_url, 'g');
102    new_response_headers.set("WWW-Authenticate", response_headers.get("WWW-Authenticate").replace(re, workers_url));
103  }
104
105  if (new_response_headers.get("Location")) {
106    return httpHandler(e.request, new_response_headers.get("Location"))
107  }
108
109  let response = new Response(original_text, {
110            status,
111            headers: new_response_headers
112        })
113  return response;
114  
115}
116
117
118/**
119 * @param {Request} req
120 * @param {string} pathname
121 */
122function httpHandler(req, pathname) {
123    const reqHdrRaw = req.headers
124
125    // preflight
126    if (req.method === 'OPTIONS' &&
127        reqHdrRaw.has('access-control-request-headers')
128    ) {
129        return new Response(null, PREFLIGHT_INIT)
130    }
131
132    let rawLen = ''
133
134    const reqHdrNew = new Headers(reqHdrRaw)
135
136    const refer = reqHdrNew.get('referer')
137
138    let urlStr = pathname
139    
140    const urlObj = newUrl(urlStr)
141
142    /** @type {RequestInit} */
143    const reqInit = {
144        method: req.method,
145        headers: reqHdrNew,
146        redirect: 'follow',
147        body: req.body
148    }
149    return proxy(urlObj, reqInit, rawLen, 0)
150}
151
152
153/**
154 *
155 * @param {URL} urlObj
156 * @param {RequestInit} reqInit
157 */
158async function proxy(urlObj, reqInit, rawLen) {
159    const res = await fetch(urlObj.href, reqInit)
160    const resHdrOld = res.headers
161    const resHdrNew = new Headers(resHdrOld)
162
163    // verify
164    if (rawLen) {
165        const newLen = resHdrOld.get('content-length') || ''
166        const badLen = (rawLen !== newLen)
167
168        if (badLen) {
169            return makeRes(res.body, 400, {
170                '--error': `bad len: ${newLen}, except: ${rawLen}`,
171                'access-control-expose-headers': '--error',
172            })
173        }
174    }
175    const status = res.status
176    resHdrNew.set('access-control-expose-headers', '*')
177    resHdrNew.set('access-control-allow-origin', '*')
178    resHdrNew.set('Cache-Control', 'max-age=1500')
179    
180    resHdrNew.delete('content-security-policy')
181    resHdrNew.delete('content-security-policy-report-only')
182    resHdrNew.delete('clear-site-data')
183
184    return new Response(res.body, {
185        status,
186        headers: resHdrNew
187    })
188}

使用

临时使用

1docker pull docker-0.unsee.tech/istio/distroless

配置文件

修改 /etc/docker/daemon.json 文件。

 1# 写入配置文件
 2sudo tee /etc/docker/daemon.json <<-'EOF'
 3{
 4    "registry-mirrors": [
 5    	"https://docker-0.unsee.tech",
 6        "https://docker-cf.registry.cyou",
 7        "https://docker.1panel.live"
 8    ]
 9}
10EOF
11
12# 重启docker服务
13sudo systemctl daemon-reload && sudo systemctl restart docker

参考

自建Docker镜像加速

https://blog.grew.cc/posts/c3e9d8a/

作者

Tom

创建时间

2025-02-04

最后更新时间

2025-02-04

许可协议

CC BY 4.0