使用Cloudflare Workers自建完全免费Docker镜像

本方法 不需要服务器 也不需要域名

自月初docker镜像被封禁之后,国内的镜像源也同步都停掉了。国内服务器拉取镜像变得完全不方便。

目前有几种方法

  1. 挂代理, 给配置文件增加代理proxy配置;
  2. 自建转发服务器,配置文件增加registry-mirrors镜像源:
    1. 自建服务器,给nginx配置反向代理, 如crproxy;
    2. Cloudflare Workers免费资源,手动写请求转发代码,如cloudflare-docker-proxy

最终我选择了Cloudflare Workers方式,因为这个方法完全不需要服务器,甚至也不需要域名,只需要注册一个cloudflare帐号就能用。

我真的服了cloudflare也是太大方了,真的什么都给大家免费用。

基本原理是在cloudflare上建一个Worker,将代理转发的代码放进去执行就行了。平台会自动生成一个公网域名,在docker配置里将这个域名设置成镜像地址就行。如果有自己的域名,可以绑定就行。比如我绑定了域名 dhub.xjp.in

大家可以随便用,反正免费。到配置里加registry-mirrors配置就可以了,然后重启docker服务

1
2
3
4
5
{
"registry-mirrors": [
"dhub.xjp.in"
]
}

具体配置文件在哪,不同系统不同安装方式不一样,大家自己看文档就行。

如果不想改配置,可以拉取镜像的时候加上这个域名,比如这样也行:

1
docker pull dhub.xjp.in/nginx:1-alpine

登录https://dash.cloudflare.com/之后,进Cloudflare Workers页面,直接创建一个worker。

然后一路保存,完成创建,然后部署。

到设置页面,可以看到一个域名,访问的话会是一个Hello World。同时这里可以绑定域名,不过要求的域名必须是已经绑定到cloudflare上的。不绑定域名后续的镜像也能正常使用。

然后进入代码编辑页,将cloudflare-docker-proxy生成的代码贴进去就行了。实际上就一个代码文件,但是硬是创建了一个项目,又臭又长。

我以这个项目为基础,手动改了一个配置。源项目里包含了其他镜像,这里就没保留。

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
addEventListener("fetch", (event) => {
event.passThroughOnException();
event.respondWith(handleRequest(event.request));
});

const dockerHub = "https://registry-1.docker.io";

const routes = {
// 这里要改成你用来访问的域名,可以用它拉取容器镜像
"dhub.xjp.in": dockerHub,
};

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

return "";
}

async function handleRequest(request) {
const url = new URL(request.url);
const upstream = routeByHosts(url.hostname);
if (upstream === "") {
return new Response(
JSON.stringify({
routes: routes,
}),
{
status: 404,
}
);
}
const isDockerHub = upstream == dockerHub;
const authorization = request.headers.get("Authorization");
if (url.pathname == "/v2/") {
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://${url.hostname}/v2/auth",service="cloudflare-docker-proxy"`
);
return new Response(JSON.stringify({ message: "UNAUTHORIZED" }), {
status: 401,
headers: headers,
});
} else {
return resp;
}
}
// get token
if (url.pathname == "/v2/auth") {
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);
}
// 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, 301);
}
}
// foward requests
const newUrl = new URL(upstream + url.pathname);
const newReq = new Request(newUrl, {
method: request.method,
headers: request.headers,
redirect: "follow",
});
return await fetch(newReq);
}

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 });
}

上面的代码也保存到 Gist docker-proxy.worker.js

部署之后就可以成功拉取镜像了。

1
docker pull dhub.xjp.in/nginx:1-alpine