一、 项目背景与痛点分析

众所周知,2024年6月起,由于 DNS 污染和 SNI 阻断,境内拉取 Docker 官方镜像变得异常困难。

作为 O&M 工程师或开发者,稳定、高速的镜像源是日常业务部署(尤其是 K8s 集群维护和容器化 CI/CD)的刚需。

目前市面上常见的几种过渡方案,往往存在致命缺陷:

1. 本地代理软件

很多人尝试通过 export https_proxy 或配置 Docker Daemon 的 http-proxy.conf 来走本地代理(如 127.0.0.1:7890)。

  • 痛点: Docker 守护进程经常“不听话”漏发代理请求;受限于本地梯子节点的线路质量和带宽,拉取大镜像(如 LLM 推理环境)时极其缓慢。
1
2
mkdir /usr/lib/systemd/system/docker.service.d
vi /usr/lib/systemd/system/docker.service.d/http-proxy.conf
1
2
3
4
[Service]
Environment="HTTP_PROXY=http://127.0.0.1:7897"
Environment="HTTPS_PROXY=http://127.0.0.1:7897"
Environment="NO_PROXY=localhost,127.0.0.1,.example.com"
1
2
sudo systemctl daemon-reload
sudo systemctl restart docker
1
systemctl show docker --property Environment

2. 公共镜像源

  • 痛点: 免费的往往最贵。速度无保障、限制并发、随时可能因被滥用而遭到 GFW 封禁(前段时间各大高校源和知名企业源纷纷下架就是前车之鉴)。另外,部分源的同步机制落后,导致拉不到最新镜像。

境内 Docker 镜像状态监控

3. 常规开源自建方案

固然也有一些开源项目可以自己部署,不过也还是会遇到问题

开源项目:

遇到的问题:

  • Docker有时候也不走代理
  • 有的开源项目使用CloudflareWorker ,可能被制裁
  • Fofa等网络空间搜索引擎抓取,为别人做嫁衣

  • 痛点: 纯 CF Worker 方案极易触碰免费额度天花板,且容易被封禁域名;纯反代方案则会因为 Docker Hub 的 6小时速率限制(Rate Limit) 导致拉取失败;此外,直接暴露的反代接口很容易被 Fofa 等空间测绘引擎抓取,最终沦为“公共汽车”,耗尽你的服务器流量。


二、 破局架构:一套高可用、防滥用的私有镜像源

思路来源:nodeseek论坛 ,让Claude 改了改。修复了直接拉取官方镜像 library 会失败的问题。

为了彻底解决“速率限制”、“流量消耗”和“访问速度”三大难题, Claude 设计了这套基于 甲骨文云 (VPS) + Nginx + Cloudflare CDN + CF Worker (动态 Token 池) 的架构。

  1. Nginx 流量分发与认证拦截: 使用 Nginx 作为核心网关。屏蔽上游 Docker Hub 的认证请求,将其指向我们自己的接口。
  2. Cloudflare CDN 极致缓存:
    • Blob 数据(镜像层): 基于 SHA256 内容寻址,属于不可变数据。通过 CF 缓存 30 天以上,极大节省甲骨文服务器与 Docker Hub 的流量。
    • Latest 标签: 绝对不缓存,保证每次都能拉到最新版本。
    • 具名 Tag(如 :1.18): 缓存 1 小时,兼顾更新与命中率。
  3. CF Worker 动态 Token 池: 拦截 /token 路由,不再使用单一的匿名或固定账号。允许用户通过前端页面提交个人 Token 加入 KV 数据库(保留1小时自动销毁),每次请求随机抽取 Token 授权,完美绕过 6 小时限流机制。
  4. 自适应极简 Web UI: 提供一个精美的引导页,集成镜像源配置命令和测速选优脚本,方便推广与自用。


三、 服务端部署实战

1. 准备工作

  • 一台海外 VPS(推荐不限流量的机器,如甲骨文云)。
  • 操作系统推荐使用你熟悉的 CentOS 或 Debian 环境。
  • 域名准备:hub.bravexist.eu.org,并托管至 Cloudflare。

2. nginx_conf

这部分是核心。我们需要对不同的 Docker Registry API 路由做精细化处理。

创建或修改站点的 Nginx 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
server {
listen 80;
listen [::]:80;
listen 443 ssl;
listen [::]:443 ssl;
server_name hub.bravexist.cn;

http2 on;
if ($scheme = http) { return 301 https://$host$request_uri; }

ssl_certificate /www/sites/hub.bravexist.cn/ssl/fullchain.pem;
ssl_certificate_key /www/sites/hub.bravexist.cn/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

error_page 497 https://$host$request_uri;
add_header Strict-Transport-Security "max-age=31536000";

resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;

gzip on;
gzip_min_length 1k;
gzip_types text/plain application/javascript text/css application/json;

# ==================================================================
# 1. Token 认证 —— 绝对不缓存
# ==================================================================
location /token {
proxy_pass https://auth.docker.io;
proxy_set_header Host auth.docker.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;
add_header Cache-Control "no-store, no-cache, must-revalidate" always;
}

# ==================================================================
# 2. Blob 层 —— 内容寻址,完全不可变,CF 缓存 30 天
# ==================================================================
location ~ ^/v2/.+/blobs/sha256: {
proxy_pass https://registry-1.docker.io;
proxy_set_header Host registry-1.docker.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;
proxy_connect_timeout 900;
proxy_send_timeout 900;
proxy_read_timeout 900;
proxy_buffering off;
proxy_request_buffering off;

# 屏蔽上游认证,替换为自己的 Token 接口
proxy_hide_header www-authenticate;
add_header www-authenticate 'Bearer realm="https://$host/token",service="registry.docker.io"' always;

proxy_hide_header Cache-Control;
proxy_hide_header Pragma;

add_header Cache-Control "public, max-age=300, s-maxage=2592000";
add_header CF-Cache-Tag "docker-blob";

gzip off;

proxy_intercept_errors on;
recursive_error_pages on;
error_page 301 302 307 = @handle_redirect;
}

# ==================================================================
# 3. Manifest —— 按 digest 引用(不可变),CF 缓存 30 天
# ==================================================================
location ~ ^/v2/.+/manifests/sha256: {
proxy_pass https://registry-1.docker.io;
proxy_set_header Host registry-1.docker.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;

proxy_hide_header www-authenticate;
add_header www-authenticate 'Bearer realm="https://$host/token",service="registry.docker.io"' always;

proxy_hide_header Cache-Control;
proxy_hide_header Pragma;
add_header Cache-Control "public, max-age=300, s-maxage=2592000";
add_header CF-Cache-Tag "docker-manifest-digest";

gzip off;
}

# ==================================================================
# 4. Manifest —— latest 标签,绝对不缓存!
# ==================================================================
location ~ ^/v2/.+/manifests/latest {
proxy_pass https://registry-1.docker.io;
proxy_set_header Host registry-1.docker.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;

proxy_hide_header www-authenticate;
add_header www-authenticate 'Bearer realm="https://$host/token",service="registry.docker.io"' always;

add_header Cache-Control "no-store, no-cache, must-revalidate" always;
add_header Pragma "no-cache" always;

gzip off;
}

# ==================================================================
# 5. Manifest —— 其他具名 tag
# ==================================================================
location ~ ^/v2/.+/manifests/ {
proxy_pass https://registry-1.docker.io;
proxy_set_header Host registry-1.docker.io;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;

proxy_hide_header www-authenticate;
add_header www-authenticate 'Bearer realm="https://$host/token",service="registry.docker.io"' always;

proxy_hide_header Cache-Control;
proxy_hide_header Pragma;
add_header Cache-Control "public, max-age=300, s-maxage=3600";
add_header CF-Cache-Tag "docker-manifest-tag";

gzip off;
}

# ==================================================================
# 6. /v2/ 根路径(ping/catalog 等)—— 不缓存
# ==================================================================
location /v2/ {
proxy_pass https://registry-1.docker.io;
proxy_set_header Host registry-1.docker.io;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;

proxy_hide_header www-authenticate;
add_header www-authenticate 'Bearer realm="https://$host/token",service="registry.docker.io"' always;
add_header Cache-Control "no-store" always;
}

# ==================================================================
# 7. 处理 Docker Hub 的 S3 重定向(Blob 实际下载)
# ==================================================================
location @handle_redirect {
internal;
set $redirect_uri '$upstream_http_location';
proxy_pass $redirect_uri;

proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_request_buffering off;
proxy_connect_timeout 900;
proxy_send_timeout 900;
proxy_read_timeout 900;

proxy_pass_request_headers off;
proxy_set_header Range $http_range;
proxy_set_header If-Range $http_if_range;
proxy_set_header User-Agent $http_user_agent;
proxy_set_header Accept-Encoding "";

# 屏蔽 AWS S3 的防缓存污染
proxy_hide_header Cache-Control;
proxy_hide_header Pragma;
proxy_hide_header x-amz-request-id;
proxy_hide_header x-amz-id-2;
proxy_hide_header Set-Cookie;
proxy_ignore_headers Cache-Control Expires Set-Cookie;

add_header Cache-Control "public, max-age=300, s-maxage=2592000";
add_header CF-Cache-Tag "docker-blob";

gzip off;
}

# ==================================================================
# 8. UI / 搜索(Docker Hub 网页)
# ==================================================================
location /search {
proxy_pass https://hub.docker.com/search;
proxy_set_header Host hub.docker.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header User-Agent $http_user_agent;
proxy_set_header Cookie $http_cookie;
proxy_set_header Referer https://hub.docker.com/;
proxy_set_header Origin https://hub.docker.com;
proxy_pass_header Set-Cookie;
proxy_cookie_domain hub.docker.com $host;
proxy_cookie_domain .docker.com $host;
proxy_redirect https://hub.docker.com/ /;
proxy_redirect https://docker.com/ /;
proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;
proxy_http_version 1.1;
proxy_set_header Connection "";
}

location ~ ^/(layers/|repositories/|namespaces/|r/|api/|auth/|v2/user/|v2/orgs/|v2/users/|v2/namespaces/|v2/repositories/|v2/feature-flags/|v2/events/|v2/jwt/|assets/|static/|_next/|_edge/|_vercel/|images/|fonts/|__nextjs/|explore/|extensions/|orgs/|u/|_/|search-federation-graphql/|billing/|settings/|dashboard/|admin/|account/|feature-flags/|sso/|consent/|metrics/|hub/|pricing/|favicon|sitemap|robots) {
proxy_pass https://hub.docker.com;
proxy_set_header Host hub.docker.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header User-Agent $http_user_agent;
proxy_set_header Cookie $http_cookie;
proxy_set_header Referer https://hub.docker.com/;
proxy_set_header Origin https://hub.docker.com;
proxy_pass_header Set-Cookie;
proxy_cookie_domain hub.docker.com $host;
proxy_cookie_domain .docker.com $host;
proxy_redirect https://hub.docker.com/ /;
proxy_redirect https://docker.com/ /;
proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;
proxy_buffering on;
proxy_http_version 1.1;
proxy_set_header Connection "";
}

# ==================================================================
# 9. UI 兜底:本地文件找不到就代理到 hub.docker.com
# (修复 Docker Hub 前端 "Error ID: xxx" 问题)
# ==================================================================
location @docker_ui_fallback {
proxy_pass https://hub.docker.com;
proxy_set_header Host hub.docker.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header User-Agent $http_user_agent;
proxy_set_header Cookie $http_cookie;
proxy_set_header Referer https://hub.docker.com/;
proxy_set_header Origin https://hub.docker.com;
proxy_pass_header Set-Cookie;
proxy_cookie_domain hub.docker.com $host;
proxy_cookie_domain .docker.com $host;
proxy_redirect https://hub.docker.com/ /;
proxy_redirect https://docker.com/ /;
proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;
proxy_http_version 1.1;
proxy_set_header Connection "";
}

# ==================================================================
# 0. 本地主页 (index.html)
# ==================================================================
root /www/sites/hub.bravexist.cn/index;
index index.html index.htm;

location = / {
try_files /index.html =404;
}

location / {
try_files $uri $uri/ @docker_ui_fallback;
}
}

3. cf-cached

光有 Nginx 的 Cache-Control 头还不够,我们需要在 Cloudflare 控制台中配置 缓存规则 (Cache Rules),强制接管并长期缓存大文件。

路径: Cloudflare -> 缓存 -> Cache Rules -> Create rule。

规则优先级(从上到下):

规则名称 匹配表达式 缓存设置 说明
docker-no-cache (http.request.uri.path eq "/token") or (ends_with(http.request.uri.path, "/manifests/latest")) 绕过缓存 动态Token和Latest标签绝对不缓存
docker-immutable (http.request.uri.path contains "/blobs/sha256:") or (http.request.uri.path contains "/manifests/sha256:") 符合缓存条件 边缘TTL: 1年 浏览器TTL: 1年 接受强ETag: 开启 重新验证时提供过时内容: 开启 镜像核心数据,不可变,长期缓存省流量
docker-tags (http.request.uri.path contains "/manifests/") and not (http.request.uri.path contains "/manifests/sha256:") and not (ends_with(http.request.uri.path, "/manifests/latest")) 符合缓存条件 边缘TTL: 1小时 浏览器TTL: 5分钟 其他具名版本号标签,短期缓存

4. cf-worker-token-health

为了彻底摆脱单 IP / 单账号被 Docker Hub 限速(6小时只能拉取少量镜像),我们利用 CF Worker 做请求劫持,并引入社区共享 Token 机制。

  1. 在 CF 创建一个 KV 命名空间,命名为 DOCKER_TOKENS
  2. 创建 Worker,绑定该 KV 命名空间(变量名也必须为 DOCKER_TOKENS)。
  3. 并在 Worker 设置中配置路由拦截,匹配:hub.bravexist.eu.org/token*hub.bravexist.eu.org/healthhub.bravexist.eu.org/submit

Worker 核心逻辑说明(详见完整源码):

  • /token 路由: 检查请求是否有 Authorization 头,没有则从内置 BACKUP_TOKENS 与 KV 中用户提交的 Token 构成的资源池中随机抽取一个,并注入 Header 转发至官方 auth.docker.io
  • /submit 路由: 接收社区用户提交的 PAT (Personal Access Token),验证其有效性(能否获取 > 100 limit 的配额),验证成功后存入 KV,设置 TTL 为 3600 秒(1小时后自动销毁,保障安全)。
  • /health 路由: 实时监控备用节点与用户贡献池的配额消耗情况。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
// ====================================================================
// Docker Hub 镜像加速 Worker - 社区贡献版
// ====================================================================
const BACKUP_TOKENS = [
"Basic cWlhbmtvbmc6ZGNrcl9wYXRfcmhyNlVWT3YzV2g0TG41M0ZqSTFsNVp1bGk0"
];

const CONFIG = {
TOKEN_TTL: 3600, // 用户 token 保留 1 小时
MAX_USER_TOKENS: 50, // KV 中用户 token 上限
IP_RATELIMIT_PER_HOUR: 10, // 单 IP 每小时最多提交次数
IP_RATELIMIT_TTL: 3600,
VALIDATE_TIMEOUT: 8000, // 校验超时 8 秒
};

export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);

if (url.pathname === '/health') {
return handleHealth(env);
}

if (url.pathname === '/submit') {
if (request.method === 'GET') return htmlResponse(renderSubmitPage());
if (request.method === 'POST') return handleSubmit(request, env);
return new Response('Method Not Allowed', { status: 405 });
}

if (url.pathname.includes('/token')) {
return handleTokenRoute(request, env);
}

return fetch(request);
}
};

// --------------------------------------------------------------------
// /token 路由:从 BACKUP + 用户贡献池中随机挑一个
// --------------------------------------------------------------------
async function handleTokenRoute(request, env) {
const newHeaders = new Headers(request.headers);
if (!newHeaders.has("Authorization")) {
const userTokens = await getUserTokens(env);
const pool = [...BACKUP_TOKENS, ...userTokens];
if (pool.length > 0) {
newHeaders.set("Authorization", pool[Math.floor(Math.random() * pool.length)]);
}
}
return fetch(new Request(request, { headers: newHeaders }));
}

async function getUserTokens(env) {
try {
const list = await env.DOCKER_TOKENS.list({ prefix: 'token:' });
return list.keys.map(k => k.metadata?.auth).filter(Boolean);
} catch (e) {
return [];
}
}

// --------------------------------------------------------------------
// /submit POST:接收用户提交,校验、限流、入库
// --------------------------------------------------------------------
async function handleSubmit(request, env) {
let data;
try { data = await request.json(); }
catch { return jsonResponse({ success: false, error: '请求格式错误' }, 400); }

const username = String(data.username || '').trim();
const password = String(data.password || '').trim();

// 1. 基础格式校验
if (!username || !password) {
return jsonResponse({ success: false, error: '用户名和密码/令牌不能为空' }, 400);
}
if (username.length > 100 || password.length > 300) {
return jsonResponse({ success: false, error: '输入内容过长' }, 400);
}
if (!/^[A-Za-z0-9_.-]+$/.test(username)) {
return jsonResponse({ success: false, error: '用户名格式不正确' }, 400);
}

// 2. IP 频率限制(无论成功失败都计数,防刷)
const ip = request.headers.get('CF-Connecting-IP') || 'unknown';
const rlKey = `rl:${ip}`;
const rlCount = parseInt(await env.DOCKER_TOKENS.get(rlKey) || '0', 10);
if (rlCount >= CONFIG.IP_RATELIMIT_PER_HOUR) {
return jsonResponse({ success: false, error: '提交过于频繁,请 1 小时后再试' }, 429);
}
await env.DOCKER_TOKENS.put(rlKey, String(rlCount + 1), { expirationTtl: CONFIG.IP_RATELIMIT_TTL });

// 3. 容量上限
const list = await env.DOCKER_TOKENS.list({ prefix: 'token:' });
if (list.keys.length >= CONFIG.MAX_USER_TOKENS) {
return jsonResponse({ success: false, error: '令牌池已满,请稍后再试' }, 503);
}

// 4. 构造 Basic auth 并校验
const basicAuth = 'Basic ' + btoa(`${username}:${password}`);
const v = await validateCredentials(basicAuth);
if (!v.ok) {
return jsonResponse({ success: false, error: `验证失败:${v.reason}` }, 401);
}

// 5. 入库(用哈希做 key 自动去重)
const hash = await sha256Hex(basicAuth);
try {
await env.DOCKER_TOKENS.put(`token:${hash}`, '1', {
expirationTtl: CONFIG.TOKEN_TTL,
metadata: { auth: basicAuth, user: username, ts: Date.now() }
});
} catch (e) {
return jsonResponse({ success: false, error: '存储失败' }, 500);
}

return jsonResponse({
success: true,
message: `令牌添加成功!感谢 ${username} 的贡献,将在 ${CONFIG.TOKEN_TTL / 60} 分钟后自动清除。`,
limit: v.limit,
remaining: v.remaining
});
}

// --------------------------------------------------------------------
// 凭证校验:必须能换到"认证态"token(limit > 100)
// --------------------------------------------------------------------
async function validateCredentials(basicAuth) {
let tokenData;
try {
const r = await fetch(
'https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/alpine:pull',
{ headers: { "Authorization": basicAuth }, signal: AbortSignal.timeout(CONFIG.VALIDATE_TIMEOUT) }
);
if (r.status === 401) return { ok: false, reason: '用户名或密码/令牌错误' };
if (!r.ok) return { ok: false, reason: `Docker Hub 返回 HTTP ${r.status}` };
tokenData = await r.json();
if (!tokenData.token) return { ok: false, reason: '未获取到有效令牌' };
} catch (e) {
return { ok: false, reason: e.name === 'TimeoutError' ? '认证服务响应超时' : '无法连接认证服务' };
}

try {
const r = await fetch(
'https://registry-1.docker.io/v2/library/alpine/manifests/latest',
{
method: 'HEAD',
headers: {
"Authorization": `Bearer ${tokenData.token}`,
"Accept": "application/vnd.docker.distribution.manifest.v2+json"
},
signal: AbortSignal.timeout(CONFIG.VALIDATE_TIMEOUT)
}
);
const limitH = r.headers.get("ratelimit-limit");
const remainH = r.headers.get("ratelimit-remaining");
const limit = limitH ? parseInt(limitH.split(';')[0], 10) : 0;
const remaining = remainH ? parseInt(remainH.split(';')[0], 10) : 0;

if (limit > 0 && limit <= 100) {
return { ok: false, reason: '仅取得匿名配额,凭证未生效' };
}
return { ok: true, limit: limit || 200, remaining };
} catch (e) {
return { ok: false, reason: e.name === 'TimeoutError' ? 'Registry 响应超时' : '无法连接 Registry' };
}
}

// --------------------------------------------------------------------
// 工具函数
// --------------------------------------------------------------------
async function sha256Hex(str) {
const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str));
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('').slice(0, 24);
}

function jsonResponse(obj, status = 200) {
return new Response(JSON.stringify(obj), {
status,
headers: { "Content-Type": "application/json; charset=UTF-8", "Cache-Control": "no-store" }
});
}

function htmlResponse(html) {
return new Response(html, {
headers: { "Content-Type": "text/html; charset=UTF-8", "Cache-Control": "no-store" }
});
}

// --------------------------------------------------------------------
// /health 面板(在你原版基础上加了贡献池统计和入口)
// --------------------------------------------------------------------
async function handleHealth(env) {
const checkAccount = async (auth, index) => {
try {
const tokenRes = await fetch(
"https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/alpine:pull",
{ headers: { "Authorization": auth }, signal: AbortSignal.timeout(3000) }
);
const tokenData = await tokenRes.json();
const manifestRes = await fetch(
"https://registry-1.docker.io/v2/library/alpine/manifests/latest",
{
method: "HEAD",
headers: {
"Authorization": `Bearer ${tokenData.token}`,
"Accept": "application/vnd.docker.distribution.manifest.v2+json"
},
signal: AbortSignal.timeout(3000)
}
);
const limit = manifestRes.headers.get("ratelimit-limit")?.split(';')[0] || "200";
const remaining = manifestRes.headers.get("ratelimit-remaining")?.split(';')[0] || "0";
return {
name: `节点账号 0${index + 1}`,
status: manifestRes.status === 200 ? "运行正常" : "异常",
limit: parseInt(limit, 10),
remaining: parseInt(remaining, 10)
};
} catch (e) {
return {
name: `节点账号 0${index + 1}`,
status: e.name === 'TimeoutError' ? "请求超时(>3秒)" : "网络异常",
limit: 200, remaining: 0
};
}
};

const [results, userTokenCount] = await Promise.all([
Promise.all(BACKUP_TOKENS.map((auth, i) => checkAccount(auth, i))),
env.DOCKER_TOKENS.list({ prefix: 'token:' }).then(l => l.keys.length).catch(() => 0)
]);

const now = new Date();
const localTime = new Date(now.getTime() + (8 * 60 + now.getTimezoneOffset()) * 60000);
const timeString = localTime.toLocaleString('zh-CN', { hour12: false });

let cardsHtml = "", totalRemaining = 0, totalLimit = 0;
results.forEach(acc => {
totalRemaining += acc.remaining; totalLimit += acc.limit;
const percent = Math.round((acc.remaining / acc.limit) * 100);
const color = percent > 50 ? "#10B981" : percent > 20 ? "#F59E0B" : "#EF4444";
cardsHtml += `
<div class="card">
<div class="card-header">
<h3>${acc.name}</h3>
<span class="status ${acc.status === '运行正常' ? 'status-ok' : 'status-err'}">${acc.status}</span>
</div>
<div class="progress-info">剩余:<strong>${acc.remaining} / ${acc.limit}</strong> 次</div>
<div class="progress-bar-bg"><div class="progress-bar" style="width:${percent}%;background-color:${color};"></div></div>
<div style="margin-top:10px;font-size:12px;color:#9ca3af;">配额在消耗后 6 小时滚动恢复</div>
</div>`;
});

const html = `<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Docker Hub 加速节点状态</title>
<style>
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#f3f4f6;color:#1f2937;margin:0;padding:20px}
.container{max-width:800px;margin:0 auto}
.header{text-align:center;margin-bottom:20px}
.header h1{margin:0 0 10px;color:#111827}
.time-panel{text-align:center;margin-bottom:20px;font-size:14px;color:#4b5563}
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:20px}
.card{background:#fff;border-radius:12px;padding:20px;box-shadow:0 4px 6px -1px rgba(0,0,0,.1)}
.card-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;border-bottom:1px solid #e5e7eb;padding-bottom:10px}
.card-header h3{margin:0;font-size:16px;color:#374151}
.status{font-size:12px;padding:4px 8px;border-radius:999px;font-weight:500}
.status-ok{background:#d1fae5;color:#065f46}.status-err{background:#fee2e2;color:#991b1b}
.progress-info{font-size:14px;margin-bottom:8px;color:#4b5563}
.progress-bar-bg{height:8px;background:#e5e7eb;border-radius:4px;overflow:hidden}
.progress-bar{height:100%;border-radius:4px;transition:width .5s ease}
.summary{background:linear-gradient(135deg,#3b82f6,#2563eb);color:#fff;padding:20px;border-radius:12px;margin-bottom:20px;text-align:center;box-shadow:0 4px 6px -1px rgba(59,130,246,.5)}
.summary h2{margin:0 0 5px;font-size:36px}.summary p{margin:0;opacity:.9;font-size:14px}
.contrib{background:#ecfdf5;border:1px solid #a7f3d0;color:#065f46;padding:15px 20px;border-radius:12px;margin-bottom:20px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px}
.contrib a{padding:8px 16px;background:#10B981;color:#fff;border-radius:8px;text-decoration:none;font-weight:500;font-size:14px}
.contrib a:hover{background:#059669}
</style></head><body>
<div class="container">
<div class="header"><h1>Docker Hub 镜像加速节点</h1>
<div class="time-panel">当前查询时间:<strong>${timeString}</strong>(实时数据)</div></div>
<div class="contrib">
<span>💡 社区贡献池:当前有 <strong>${userTokenCount}</strong> 个用户贡献的令牌正在服务中(1 小时自动滚动)</span>
<a href="/submit">🎁 贡献我的 Docker Hub 令牌</a>
</div>
<div class="summary"><h2>${totalRemaining} / ${totalLimit}</h2><p>集群总可用拉取配额</p></div>
<div class="grid">${cardsHtml}</div>
</div></body></html>`;
return new Response(html, { headers: { "Content-Type": "text/html;charset=UTF-8" } });
}

// --------------------------------------------------------------------
// /submit 页面 HTML
// --------------------------------------------------------------------
function renderSubmitPage() {
return `<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>贡献 Docker Hub 令牌 - 共享加速池</title>
<style>
*{box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:#f3f4f6;color:#1f2937;margin:0;padding:20px}
.container{max-width:560px;margin:0 auto}
.header{text-align:center;margin-bottom:24px}
.header h1{margin:0 0 8px;color:#111827}
.header p{color:#6b7280;margin:0}
.card{background:#fff;border-radius:12px;padding:28px;box-shadow:0 4px 6px -1px rgba(0,0,0,.1)}
.notice{background:#fef3c7;border:1px solid #fcd34d;color:#92400e;padding:14px 16px;border-radius:8px;margin-bottom:20px;font-size:14px;line-height:1.6}
.notice b{color:#78350f}
.notice a{color:#78350f;text-decoration:underline}
label{display:block;margin-bottom:6px;font-weight:500;color:#374151;font-size:14px}
input{width:100%;padding:10px 12px;border:1px solid #d1d5db;border-radius:8px;font-size:14px;font-family:inherit}
input:focus{outline:none;border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.2)}
.field{margin-bottom:18px}
.hint{margin-top:6px;font-size:12px;color:#6b7280}
.btn{width:100%;padding:12px;background:linear-gradient(135deg,#3b82f6,#2563eb);color:#fff;border:none;border-radius:8px;font-size:15px;font-weight:500;cursor:pointer;transition:.2s}
.btn:hover{box-shadow:0 4px 12px rgba(59,130,246,.4)}
.btn:disabled{opacity:.6;cursor:not-allowed}
.result{margin-top:18px;padding:14px 16px;border-radius:8px;font-size:14px;display:none}
.result.ok{background:#d1fae5;border:1px solid #6ee7b7;color:#065f46;display:block}
.result.err{background:#fee2e2;border:1px solid #fca5a5;color:#991b1b;display:block}
.steps{background:#f9fafb;border-radius:8px;padding:16px 20px;margin-bottom:20px;font-size:14px;line-height:1.7;color:#4b5563}
.steps ol{margin:8px 0 0;padding-left:22px}
.steps a{color:#2563eb}
.back{display:inline-block;margin-top:16px;color:#6b7280;text-decoration:none;font-size:14px}
</style></head><body>
<div class="container">
<div class="header"><h1>🎁 贡献令牌</h1><p>加入社区加速池,共享你的拉取配额</p></div>
<div class="card">
<div class="notice">
<b>隐私保护:</b>你的凭证会在 KV 中 <b>1 小时</b> 后自动清除,不会写入日志或持久化存储。强烈建议使用 Docker Hub 的
<a href="https://app.docker.com/settings/personal-access-tokens" target="_blank" rel="noopener">Personal Access Token (PAT)</a>(权限选 <b>Public Repo Read-only</b>),可随时在官网撤销,最安全。
</div>
<div class="steps">
<b>推荐流程(生成只读 PAT):</b>
<ol>
<li>登录 <a href="https://app.docker.com/settings/personal-access-tokens" target="_blank" rel="noopener">Docker Hub → Account Settings → Personal access tokens</a></li>
<li>点击 <b>Generate new token</b></li>
<li>权限选择 <b>Public Repo Read-only</b></li>
<li>复制生成的令牌,填入下方"密码"栏</li>
</ol>
</div>
<form id="f">
<div class="field"><label for="u">Docker Hub 用户名</label>
<input type="text" id="u" autocomplete="off" required placeholder="例如:your_username"></div>
<div class="field"><label for="p">密码或 PAT 令牌</label>
<input type="password" id="p" autocomplete="off" required placeholder="推荐粘贴 Personal Access Token">
<div class="hint">无需包含 "Basic" 前缀,系统会自动完成 Base64 编码。</div></div>
<button type="submit" class="btn" id="b">提交并加入加速池</button>
</form>
<div id="r" class="result"></div>
<a href="/health" class="back">← 返回节点状态面板</a>
</div>
</div>
<script>
const f=document.getElementById('f'),b=document.getElementById('b'),r=document.getElementById('r');
f.addEventListener('submit',async e=>{
e.preventDefault();
const username=document.getElementById('u').value.trim(),password=document.getElementById('p').value.trim();
if(!username||!password)return;
b.disabled=true;b.textContent='验证中,请稍候...';r.className='result';r.textContent='';
try{
const res=await fetch('/submit',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username,password})});
const d=await res.json();
if(d.success){r.className='result ok';let m='✅ '+d.message;if(d.limit)m+=' 检测到配额:'+d.remaining+' / '+d.limit;r.textContent=m;f.reset();}
else{r.className='result err';r.textContent='❌ '+(d.error||'提交失败');}
}catch(e){r.className='result err';r.textContent='❌ 网络错误:'+e.message;}
finally{b.disabled=false;b.textContent='提交并加入加速池';}
});
</script></body></html>`;
}

5. index_home

将你编写的带有自适应域名、一键复制代码、多平台测速脚本的精美 index.html 放置于 Nginx 配置的 root 目录 /www/sites/hub.bravexist.eu.org/index 中。页面会自动识别当前域名为 hub.bravexist.eu.org

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Docker Registry Mirror</title>
<style>
:root {
--bg-color: #0f172a;
--card-bg: rgba(30, 41, 59, 0.55);
--card-bg-inner: rgba(15, 23, 42, 0.5);
--accent-color: #38bdf8;
--accent-glow: #0ea5e9;
--accent-purple: #818cf8;
--text-color: #e2e8f0;
--text-muted: #94a3b8;
--text-dim: #64748b;
--code-bg: rgba(15, 15, 25, 0.7);
--success-color: #4ade80;
--warn-color: #fbbf24;
--border-color: rgba(255, 255, 255, 0.1);
--border-hover: rgba(56, 189, 248, 0.4);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html { scroll-behavior: smooth; }
body {
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: linear-gradient(-45deg, #0f172a, #1e1b4b, #312e81, #0f172a);
background-size: 400% 400%;
animation: gradientBG 20s ease infinite;
color: var(--text-color);
min-height: 100vh;
padding: 40px 20px;
line-height: 1.6;
}
@keyframes gradientBG {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
/* 背景装饰光斑 */
body::before, body::after {
content: '';
position: fixed;
border-radius: 50%;
filter: blur(80px);
opacity: 0.4;
pointer-events: none;
z-index: 0;
}
body::before {
top: -100px; left: -100px;
width: 400px; height: 400px;
background: radial-gradient(circle, #38bdf8 0%, transparent 70%);
}
body::after {
bottom: -100px; right: -100px;
width: 500px; height: 500px;
background: radial-gradient(circle, #818cf8 0%, transparent 70%);
}
.container {
width: 100%;
max-width: 900px;
margin: 0 auto;
position: relative;
z-index: 1;
}
/* 玻璃卡片通用样式 */
.glass-card {
background: var(--card-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--border-color);
border-radius: 24px;
padding: 40px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
margin-bottom: 30px;
}
header {
text-align: center;
margin-bottom: 40px;
}
.logo-icon {
font-size: 3rem;
margin-bottom: 10px;
display: inline-block;
filter: drop-shadow(0 0 20px rgba(56, 189, 248, 0.6));
}
h1 {
font-size: 2.5rem;
font-weight: 800;
background: linear-gradient(to right, #38bdf8, #818cf8);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
margin-bottom: 10px;
text-shadow: 0 0 30px rgba(56, 189, 248, 0.3);
}
.subtitle {
color: var(--text-muted);
font-size: 1.1rem;
}
/* 域名显示条 */
.domain-badge {
display: inline-flex;
align-items: center;
gap: 10px;
margin-top: 18px;
padding: 10px 20px;
background: rgba(56, 189, 248, 0.1);
border: 1px solid rgba(56, 189, 248, 0.3);
border-radius: 999px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 0.95rem;
color: var(--accent-color);
}
.domain-badge::before {
content: '●';
color: var(--success-color);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
/* Tab 切换 */
.tabs {
display: flex;
gap: 8px;
margin-bottom: 24px;
padding: 6px;
background: rgba(15, 23, 42, 0.5);
border-radius: 14px;
border: 1px solid var(--border-color);
flex-wrap: wrap;
}
.tab-btn {
flex: 1;
min-width: 120px;
padding: 10px 16px;
background: transparent;
border: none;
color: var(--text-muted);
cursor: pointer;
border-radius: 10px;
font-size: 0.95rem;
font-weight: 500;
transition: all 0.3s;
font-family: inherit;
}
.tab-btn:hover {
color: var(--text-color);
background: rgba(255, 255, 255, 0.05);
}
.tab-btn.active {
background: linear-gradient(135deg, rgba(56, 189, 248, 0.2), rgba(129, 140, 248, 0.2));
color: var(--accent-color);
box-shadow: inset 0 0 0 1px rgba(56, 189, 248, 0.4);
}
.tab-panel { display: none; }
.tab-panel.active { display: block; animation: fadeIn 0.3s ease; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
/* 步骤标题 */
.section-title {
font-size: 1.15rem;
margin: 24px 0 14px;
color: var(--accent-color);
display: flex;
align-items: center;
gap: 12px;
font-weight: 600;
}
.section-title:first-child { margin-top: 0; }
.step-num {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: linear-gradient(135deg, var(--accent-color), var(--accent-purple));
color: #000;
border-radius: 50%;
font-size: 0.85rem;
font-weight: 700;
box-shadow: 0 0 15px rgba(56, 189, 248, 0.4);
}
.section-title .hint {
margin-left: auto;
font-size: 0.8rem;
color: var(--text-dim);
font-weight: normal;
}
/* 代码块 */
.code-wrapper {
position: relative;
margin-bottom: 20px;
background: var(--code-bg);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.05);
overflow: hidden;
transition: border-color 0.2s;
}
.code-wrapper:hover {
border-color: var(--border-hover);
}
pre {
padding: 18px 20px;
padding-right: 80px;
overflow-x: auto;
color: #d4d4d4;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.9rem;
line-height: 1.65;
}
pre::-webkit-scrollbar { height: 6px; }
pre::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
.copy-btn {
position: absolute;
top: 10px;
right: 10px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
color: var(--text-color);
padding: 6px 12px;
border-radius: 8px;
cursor: pointer;
font-size: 0.78rem;
transition: all 0.3s ease;
font-family: inherit;
backdrop-filter: blur(10px);
}
.copy-btn:hover {
background: var(--accent-color);
color: #000;
border-color: var(--accent-color);
}
.copy-btn.copied {
background: var(--success-color);
color: #000;
border-color: var(--success-color);
}
/* 提示框 */
.notice {
padding: 14px 18px;
border-radius: 12px;
margin-bottom: 20px;
font-size: 0.9rem;
line-height: 1.6;
display: flex;
gap: 12px;
align-items: flex-start;
}
.notice-info {
background: rgba(56, 189, 248, 0.08);
border: 1px solid rgba(56, 189, 248, 0.2);
color: #bae6fd;
}
.notice-warn {
background: rgba(251, 191, 36, 0.08);
border: 1px solid rgba(251, 191, 36, 0.2);
color: #fde68a;
}
.notice-icon {
font-size: 1.1rem;
flex-shrink: 0;
}
/* 可展开块 */
details {
margin-bottom: 16px;
background: rgba(15, 23, 42, 0.4);
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
}
summary {
padding: 14px 18px;
cursor: pointer;
font-weight: 500;
color: var(--text-color);
list-style: none;
display: flex;
align-items: center;
gap: 10px;
transition: background 0.2s;
}
summary::-webkit-details-marker { display: none; }
summary::before {
content: '▸';
color: var(--accent-color);
transition: transform 0.2s;
}
details[open] > summary::before { transform: rotate(90deg); }
summary:hover { background: rgba(255, 255, 255, 0.03); }
details > div { padding: 0 18px 16px; }
/* 链接 */
a { color: var(--accent-color); text-decoration: none; transition: color 0.3s; }
a:hover { color: var(--accent-purple); }
/* 底部 */
footer {
text-align: center;
padding: 30px 20px 10px;
color: var(--text-dim);
font-size: 0.88rem;
}
.footer-link {
color: var(--accent-color);
font-weight: 600;
}
.footer-link:hover {
color: var(--accent-purple);
text-shadow: 0 0 10px rgba(129, 140, 248, 0.5);
}
/* 语法高亮 */
.cmd { color: #f9c74f; }
.path { color: #9cdcfe; }
.str { color: #ce9178; }
.key { color: #9cdcfe; }
.comment { color: #6a9955; font-style: italic; }
.flag { color: #c586c0; }
/* 表格(测速示例) */
.table-wrap {
overflow-x: auto;
margin-bottom: 16px;
background: var(--code-bg);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.05);
}
table {
width: 100%;
border-collapse: collapse;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 0.85rem;
}
th, td {
padding: 10px 14px;
text-align: left;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
th {
color: var(--accent-color);
font-weight: 600;
background: rgba(56, 189, 248, 0.05);
}
td { color: #d4d4d4; }
tr:last-child td { border-bottom: none; }
tr:hover td { background: rgba(255,255,255,0.02); }
/* 响应式 */
@media (max-width: 640px) {
.glass-card { padding: 24px 18px; border-radius: 18px; }
h1 { font-size: 1.9rem; }
pre { font-size: 0.82rem; padding: 14px 16px; padding-right: 70px; }
.tabs { padding: 4px; }
.tab-btn { min-width: 80px; padding: 8px 10px; font-size: 0.85rem; }
}
</style>
</head>
<body>
<div class="container">
<!-- 顶部介绍卡 -->
<div class="glass-card">
<header>
<div class="logo-icon">🐳</div>
<h1>Docker Mirror</h1>
<p class="subtitle">高速 • 稳定 • 私有化加速服务</p>
<div class="domain-badge">
<span id="domain-display">hub.bravexist.cn</span>
</div>
</header>

<div class="section-title">
<span class="step-num">1</span>创建/编辑 Docker 配置文件
</div>
<div class="code-wrapper">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre><code><span class="cmd">sudo mkdir</span> -p /etc/docker
<span class="cmd">sudo vim</span> <span class="path">/etc/docker/daemon.json</span></code></pre>
</div>

<div class="section-title">
<span class="step-num">2</span>添加镜像源配置
</div>
<div class="code-wrapper">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre><code>{
<span class="key">"registry-mirrors"</span>: [<span class="str">"https://<span class="domain-placeholder">hub.bravexist.cn</span>"</span>]
}</code></pre>
</div>

<div class="section-title">
<span class="step-num">3</span>重启 Docker 服务
</div>
<div class="code-wrapper">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre><code><span class="cmd">sudo</span> systemctl daemon-reload
<span class="cmd">sudo</span> systemctl restart docker</code></pre>
</div>

<div class="notice notice-info">
<span class="notice-icon">💡</span>
<div>配置完成后执行 <code style="background:rgba(0,0,0,0.3);padding:2px 6px;border-radius:4px;">docker info</code>,在输出底部能看到 <b>Registry Mirrors</b> 中列出本站域名即代表生效。</div>
</div>
</div>

<!-- 速度优化卡 -->
<div class="glass-card">
<header style="margin-bottom: 24px; text-align: left;">
<h1 style="font-size: 1.6rem; margin-bottom: 6px;">⚡ 速度优化指南</h1>
<p class="subtitle" style="font-size: 0.95rem;">
Cloudflare 默认把你分配到的边缘节点,不一定是对你<b>下载速度</b>最快的那一个。通过 <b>CloudflareSpeedTest</b> 实测挑出最快 IP 并绑定 hosts,Docker 拉取速度常能提升数倍。
</p>
</header>

<div class="notice notice-warn">
<span class="notice-icon">🎯</span>
<div>
<b>为什么必须在你本机跑?</b>CF 对你快的节点,对我可能很慢——节点速度由<b>你所在网络到该节点的路由</b>决定,没有"全球最优 IP"这种东西。服务端也不会预先优选,那样既无效(缓存命中根本不回源)又属于对 CF 的滥用扫描。<br>
<span style="opacity: 0.8; font-size: 0.88rem;">优选 IP 会随路由变化漂移,建议每 1–2 周重测一次。</span>
</div>
</div>

<div class="notice notice-info">
<span class="notice-icon">💡</span>
<div><b>看哪一列?</b>测速结果里真正决定你体验的是 <b>下载速度(MB/s)</b>,不是延迟。延迟只是初筛——延迟高的节点基本不可能快,但延迟低的节点也可能因为带宽被打满而慢。挑下载速度最高的那个即可。</div>
</div>

<div class="tabs" role="tablist">
<button class="tab-btn active" onclick="switchTab(this, 'tab-linux')">🐧 Linux</button>
<button class="tab-btn" onclick="switchTab(this, 'tab-windows')">🪟 Windows</button>
<button class="tab-btn" onclick="switchTab(this, 'tab-macos')">🍎 macOS</button>
<button class="tab-btn" onclick="switchTab(this, 'tab-hosts')">📝 写入 hosts</button>
</div>

<!-- Linux -->
<div id="tab-linux" class="tab-panel active">
<div class="section-title"><span class="step-num">1</span>下载并解压</div>
<div class="code-wrapper">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre><code><span class="comment"># 建立独立目录(便于后续更新)</span>
<span class="cmd">mkdir</span> -p cfst &amp;&amp; <span class="cmd">cd</span> cfst

<span class="comment"># 下载(国内网络请用下方镜像之一替换 github.com)</span>
<span class="cmd">wget</span> -N https://github.com/XIU2/CloudflareSpeedTest/releases/download/v2.3.4/cfst_linux_amd64.tar.gz

<span class="cmd">tar</span> -zxf cfst_linux_amd64.tar.gz
<span class="cmd">chmod</span> +x cfst</code></pre>
</div>

<details>
<summary>国内下载加速镜像(任选其一替换上面的 github.com)</summary>
<div>
<div class="code-wrapper" style="margin-top: 10px;">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre><code><span class="cmd">wget</span> -N https://wget.la/https://github.com/XIU2/CloudflareSpeedTest/releases/download/v2.3.4/cfst_linux_amd64.tar.gz
<span class="cmd">wget</span> -N https://ghfast.top/https://github.com/XIU2/CloudflareSpeedTest/releases/download/v2.3.4/cfst_linux_amd64.tar.gz
<span class="cmd">wget</span> -N https://ghproxy.it/https://github.com/XIU2/CloudflareSpeedTest/releases/download/v2.3.4/cfst_linux_amd64.tar.gz
<span class="cmd">wget</span> -N https://gh-proxy.org/https://github.com/XIU2/CloudflareSpeedTest/releases/download/v2.3.4/cfst_linux_amd64.tar.gz
<span class="cmd">wget</span> -N https://cdn.gh-proxy.org/https://github.com/XIU2/CloudflareSpeedTest/releases/download/v2.3.4/cfst_linux_amd64.tar.gz</code></pre>
</div>
</div>
</details>

<div class="section-title"><span class="step-num">2</span>运行测速</div>
<div class="code-wrapper">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre><code><span class="comment"># 默认测速</span>
./cfst

<span class="comment"># 推荐参数:延迟上限 200ms,输出前 20 个</span>
./cfst <span class="flag">-tl</span> 200 <span class="flag">-dn</span> 20</code></pre>
</div>
</div>

<!-- Windows -->
<div id="tab-windows" class="tab-panel">
<div class="section-title"><span class="step-num">1</span>使用 Scoop 安装(推荐)</div>
<div class="code-wrapper">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre><code><span class="comment"># 添加中文软件源 dorado</span>
<span class="cmd">scoop</span> bucket add dorado https://github.com/chawyehsu/dorado

<span class="comment"># 安装 CloudflareSpeedTest</span>
<span class="cmd">scoop</span> install dorado/cloudflare-speedtest

<span class="comment"># 运行(在 PowerShell / CMD 里)</span>
<span class="cmd">cfst</span> <span class="flag">-tl</span> 200 <span class="flag">-dn</span> 20</code></pre>
</div>

<details>
<summary>手动下载方式(无 Scoop 环境)</summary>
<div>
<p style="margin: 10px 0; color: var(--text-muted); font-size: 0.9rem;">
访问 <a href="https://github.com/XIU2/CloudflareSpeedTest/releases" target="_blank" rel="noopener">Releases 页面</a>
下载 <code style="background:rgba(0,0,0,0.3);padding:2px 6px;border-radius:4px;">cfst_windows_amd64.zip</code>
解压后在目录内打开 PowerShell 运行:
</p>
<div class="code-wrapper">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre><code>.\CloudflareST.exe <span class="flag">-tl</span> 200 <span class="flag">-dn</span> 20</code></pre>
</div>
</div>
</details>
</div>

<!-- macOS -->
<div id="tab-macos" class="tab-panel">
<div class="section-title"><span class="step-num">1</span>下载(根据芯片选择)</div>
<div class="code-wrapper">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre><code><span class="cmd">mkdir</span> -p cfst &amp;&amp; <span class="cmd">cd</span> cfst

<span class="comment"># Apple Silicon (M1/M2/M3/M4)</span>
<span class="cmd">curl</span> -LO https://github.com/XIU2/CloudflareSpeedTest/releases/download/v2.3.4/cfst_darwin_arm64.tar.gz
<span class="cmd">tar</span> -zxf cfst_darwin_arm64.tar.gz

<span class="comment"># Intel 芯片</span>
<span class="comment"># curl -LO https://github.com/XIU2/CloudflareSpeedTest/releases/download/v2.3.4/cfst_darwin_amd64.tar.gz</span>
<span class="comment"># tar -zxf cfst_darwin_amd64.tar.gz</span>

<span class="cmd">chmod</span> +x cfst</code></pre>
</div>

<div class="section-title"><span class="step-num">2</span>运行测速</div>
<div class="code-wrapper">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre><code>./cfst <span class="flag">-tl</span> 200 <span class="flag">-dn</span> 20</code></pre>
</div>

<div class="notice notice-info">
<span class="notice-icon">🔒</span>
<div>首次运行若提示"无法打开",请到 <b>系统设置 → 隐私与安全性</b> 底部点击"仍要打开",或执行 <code style="background:rgba(0,0,0,0.3);padding:2px 6px;border-radius:4px;">xattr -d com.apple.quarantine ./cfst</code></div>
</div>
</div>

<!-- 写入 hosts -->
<div id="tab-hosts" class="tab-panel">
<div class="section-title">示例输出</div>
<p style="color: var(--text-muted); font-size: 0.9rem; margin-bottom: 12px;">
测速完成后会显示最快的若干个 IP,类似下表(实际结果因地区而异):
</p>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>IP 地址</th>
<th>丢包率</th>
<th>平均延迟</th>
<th>下载速度(MB/s)</th>
<th>地区</th>
</tr>
</thead>
<tbody>
<tr><td>104.27.200.69</td><td>0.00</td><td>146.23</td><td>28.64</td><td>LAX</td></tr>
<tr><td>172.67.60.78</td><td>0.00</td><td>139.82</td><td>15.02</td><td>SEA</td></tr>
<tr><td>104.25.140.153</td><td>0.00</td><td>146.49</td><td>14.90</td><td>SJC</td></tr>
<tr><td>104.27.192.65</td><td>0.00</td><td>140.28</td><td>14.07</td><td>LAX</td></tr>
<tr><td>172.67.62.214</td><td>0.00</td><td>139.29</td><td>12.71</td><td>LAX</td></tr>
</tbody>
</table>
</div>

<div class="section-title">写入 hosts(Linux / macOS)</div>
<p style="color: var(--text-muted); font-size: 0.9rem; margin-bottom: 12px;">
挑一个延迟低、下载速度高的 IP,追加一行到 <code style="background:rgba(0,0,0,0.3);padding:2px 6px;border-radius:4px;">/etc/hosts</code>
</p>
<div class="code-wrapper">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre><code><span class="cmd">sudo</span> sh -c <span class="str">'echo "104.27.200.69 <span class="domain-placeholder">hub.bravexist.cn</span>" &gt;&gt; /etc/hosts'</span></code></pre>
</div>

<div class="section-title">写入 hosts(Windows)</div>
<p style="color: var(--text-muted); font-size: 0.9rem; margin-bottom: 12px;">
以管理员身份编辑 <code style="background:rgba(0,0,0,0.3);padding:2px 6px;border-radius:4px;">C:\Windows\System32\drivers\etc\hosts</code>,追加:
</p>
<div class="code-wrapper">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre><code>104.27.200.69 <span class="domain-placeholder">hub.bravexist.cn</span></code></pre>
</div>

<div class="section-title">刷新 DNS 缓存</div>
<div class="code-wrapper">
<button class="copy-btn" onclick="copyCode(this)">复制</button>
<pre><code><span class="comment"># Linux (systemd-resolved)</span>
<span class="cmd">sudo</span> systemctl restart systemd-resolved

<span class="comment"># macOS</span>
<span class="cmd">sudo</span> dscacheutil -flushcache &amp;&amp; <span class="cmd">sudo</span> killall -HUP mDNSResponder

<span class="comment"># Windows</span>
ipconfig /flushdns</code></pre>
</div>

<div class="notice notice-info">
<span class="notice-icon"></span>
<div>绑定后 Docker 拉取会强制走你测出的这个 IP,绕过 CF Anycast 的默认分配。若过一段时间又变慢(说明这个节点负载升高或路由变了),重测换一个 IP 即可。</div>
</div>
</div>
</div>

<footer>
<p>
Powered by qiankong &nbsp;|&nbsp;
<a href="https://bravexist.cn" target="_blank" rel="noopener" class="footer-link">一世贪欢的私域</a>
</p>
</footer>
</div>

<script>
// 动态识别当前域名
(function() {
const host = window.location.hostname || 'hub.bravexist.cn';
document.title = 'Docker Registry Mirror - ' + host;
document.getElementById('domain-display').textContent = host;
document.querySelectorAll('.domain-placeholder').forEach(el => {
el.textContent = host;
});
})();

// 复制按钮
function copyCode(btn) {
const code = btn.parentElement.querySelector('code').innerText;
const done = () => {
const original = btn.innerText;
btn.innerText = '✓ 已复制';
btn.classList.add('copied');
setTimeout(() => {
btn.innerText = original;
btn.classList.remove('copied');
}, 1800);
};
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(code).then(done).catch(fallbackCopy);
} else {
fallbackCopy();
}
function fallbackCopy() {
const ta = document.createElement('textarea');
ta.value = code;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); done(); }
catch (e) { alert('复制失败,请手动复制'); }
document.body.removeChild(ta);
}
}

// Tab 切换
function switchTab(btn, panelId) {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById(panelId).classList.add('active');
}
</script>
</body>
</html>

6. router

1
2
3
4
/
/health
/search
/submit

四、 客户端配置与使用

作为普通用户,仅需简单的配置即可起飞。

hub.bravexist.cn

4.1 修改 Daemon 配置

这是 O&M 日常运维中最常用的方式。

1
2
sudo mkdir -p /etc/docker
sudo vi /etc/docker/daemon.json

写入我们的私服地址:

1
2
3
{
"registry-mirrors": ["https://hub.bravexist.eu.org"]
}

重启服务使其生效:

1
2
sudo systemctl daemon-reload
sudo systemctl restart docker

4.2 极致提速

由于 Cloudflare 的 Anycast 路由机制,分配给你的 IP 延迟虽低,但下载带宽不一定高。结合你的引导页提供的 CloudflareSpeedTest 脚本,可以大幅提升拉取大镜像(如 RedisInsight、LightLLM 环境)的速度。

1. 运行测速脚本获取最佳 IP (例如跑出了 104.27.200.69 为最高下载速度)。

2. 写入本机 Hosts 文件:

1
sudo sh -c 'echo "104.27.200.69 hub.bravexist.eu.org" >> /etc/hosts'

3. 刷新 DNS:

1
sudo systemctl restart systemd-resolved

通过这种方式,Docker 拉取镜像时会强制直连你优选出的高带宽 CF 节点,彻底起飞。


五、 验证环节

系统配置完后,身为 O&M 工程师,严谨的测试必不可少。

1. 验证镜像拉取

先清理本地缓存,强制重新拉取:

1
2
docker image prune -a
docker pull nginx:1.18

2. 验证 Token 池是否生效

通过模拟接口请求,检查返回的 Token 状态:

1
2
TOKEN=$(curl -s "https://hub.bravexist.eu.org/token?service=registry.docker.io&scope=repository:library/nginx:pull" | grep -o '"token":"[^"]*' | cut -d'"' -f4)
echo "获取到的 Token 前20位是: ${TOKEN:0:20}..."

3. 验证 CF 缓存是否命中

拿到 Token 和某个 Blob 的 SHA256 后,发起请求查看 Response Header:

1
2
3
BLOB_SHA256="你的sha256"
curl -I -s -H "Authorization: Bearer $TOKEN" \
"https://hub.bravexist.eu.org/v2/library/nginx/blobs/$BLOB_SHA256" | grep -i "cf-cache-status"

如果输出 CF-Cache-Status: HIT,恭喜你,你的架构已经完美运转,这套基于 K8s/Docker 的基础设施将稳如磐石。

六、镜像源汇总

当然总避免不了使用公开的镜像源,总之还是有一些价值的。

境内 Docker 镜像状态监控