1. Preparation Work#
-
Register and log in to your Cloudflare account
- Visit Cloudflare official website, create and log in to your account.
-
Purchase and configure a domain name
- Purchase a domain name you like from a domain registrar.
- Point your domain's DNS to Cloudflare.
-
Set up Cloudflare Workers
- Find the Workers tab in the Cloudflare dashboard and create a new worker.
2. Deploy Docker Image Proxy Service#
1. Get the Proxy Source Code#
// _worker.js
// Docker image repository host address
let hub_host = 'registry-1.docker.io'
// Docker authentication server address
const auth_url = 'https://auth.docker.io'
// Custom worker server address
let workers_url = 'https://your-domain'
let blockCrawlerUA = ['netcraft'];
// Select the corresponding upstream address based on the hostname
function routeByHosts(host) {
// Define the routing table
const routes = {
// Production environment
"quay": "quay.io",
"gcr": "gcr.io",
"k8s-gcr": "k8s.gcr.io",
"k8s": "registry.k8s.io",
"ghcr": "ghcr.io",
"cloudsmith": "docker.cloudsmith.io",
"nvcr": "nvcr.io",
// Testing environment
"test": "registry-1.docker.io",
};
if (host in routes) return [ routes[host], false ];
else return [ hub_host, true ];
}
/** @type {RequestInit} */
const PREFLIGHT_INIT = {
// Preflight request configuration
headers: new Headers({
'access-control-allow-origin': '*', // Allow all origins
'access-control-allow-methods': 'GET,POST,PUT,PATCH,TRACE,DELETE,HEAD,OPTIONS', // Allowed HTTP methods
'access-control-max-age': '1728000', // Preflight request cache time
}),
}
/**
* Construct response
* @param {any} body Response body
* @param {number} status Response status code
* @param {Object<string, string>} headers Response headers
*/
function makeRes(body, status = 200, headers = {}) {
headers['access-control-allow-origin'] = '*' // Allow all origins
return new Response(body, { status, headers }) // Return newly constructed response
}
/**
* Construct a new URL object
* @param {string} urlStr URL string
*/
function newUrl(urlStr) {
try {
return new URL(urlStr) // Attempt to construct a new URL object
} catch (err) {
return null // Return null if construction fails
}
}
function isUUID(uuid) {
// Define a regular expression to match UUID format
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[4][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
// Test UUID string using the regular expression
return uuidRegex.test(uuid);
}
async function nginx() {
const text = `
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>
<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>
<p><em>Thank you for using nginx.</em></p>
</body>
</html>
`
return text ;
}
export default {
async fetch(request, env, ctx) {
const getReqHeader = (key) => request.headers.get(key); // Get request headers
let url = new URL(request.url); // Parse request URL
const userAgentHeader = request.headers.get('User-Agent');
const userAgent = userAgentHeader ? userAgentHeader.toLowerCase() : "null";
if (env.UA) blockCrawlerUA = blockCrawlerUA.concat(await ADD(env.UA));
workers_url = `https://${url.hostname}`;
const pathname = url.pathname;
const hostname = url.searchParams.get('hubhost') || url.hostname;
const hostTop = hostname.split('.')[0];// Get the first part of the hostname
const checkHost = routeByHosts(hostTop);
hub_host = checkHost[0]; // Get upstream address
const fakePage = checkHost[1];
console.log(`Domain header: ${hostTop}\nProxy address: ${hub_host}\nFake homepage: ${fakePage}`);
const isUuid = isUUID(pathname.split('/')[1].split('/')[0]);
if (blockCrawlerUA.some(fxxk => userAgent.includes(fxxk)) && blockCrawlerUA.length > 0){
// Change homepage to an nginx fake page
return new Response(await nginx(), {
headers: {
'Content-Type': 'text/html; charset=UTF-8',
},
});
}
const conditions = [
isUuid,
pathname.includes('/_'),
pathname.includes('/r'),
pathname.includes('/v2/user'),
pathname.includes('/v2/orgs'),
pathname.includes('/v2/_catalog'),
pathname.includes('/v2/categories'),
pathname.includes('/v2/feature-flags'),
pathname.includes('search'),
pathname.includes('source'),
pathname === '/',
pathname === '/favicon.ico',
pathname === '/auth/profile',
];
if (conditions.some(condition => condition) && (fakePage === true || hostTop == 'docker')) {
if (env.URL302){
return Response.redirect(env.URL302, 302);
} else if (env.URL){
if (env.URL.toLowerCase() == 'nginx'){
// Change homepage to an nginx fake page
return new Response(await nginx(), {
headers: {
'Content-Type': 'text/html; charset=UTF-8',
},
});
} else return fetch(new Request(env.URL, request));
}
const newUrl = new URL("https://registry.hub.docker.com" + pathname + url.search);
// Copy original request headers
const headers = new Headers(request.headers);
// Ensure Host header is replaced with hub.docker.com
headers.set('Host', 'registry.hub.docker.com');
const newRequest = new Request(newUrl, {
method: request.method,
headers: headers,
body: request.method !== 'GET' && request.method !== 'HEAD' ? await request.blob() : null,
redirect: 'follow'
});
return fetch(newRequest);
}
// Modify requests containing %2F and %3A
if (!/%2F/.test(url.search) && /%3A/.test(url.toString())) {
let modifiedUrl = url.toString().replace(/%3A(?=.*?&)/, '%3Alibrary%2F');
url = new URL(modifiedUrl);
console.log(`handle_url: ${url}`)
}
// Handle token requests
if (url.pathname.includes('/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, request), token_parameter)
}
// Modify /v2/ request paths
if (/^\/v2\/[^/]+\/[^/]+\/[^/]+$/.test(url.pathname) && !/^\/v2\/library/.test(url.pathname)) {
url.pathname = url.pathname.replace(/\/v2\//, '/v2/library/');
console.log(`modified_url: ${url.pathname}`)
}
// Change the hostname of the request
url.hostname = hub_host;
// Construct request parameters
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 // Cache time
};
// Add Authorization header
if (request.headers.has("Authorization")) {
parameter.headers.Authorization = getReqHeader("Authorization");
}
// Initiate request and handle response
let original_response = await fetch(new Request(url, 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;
// Modify Www-Authenticate header
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));
}
// Handle redirects
if (new_response_headers.get("Location")) {
return httpHandler(request, new_response_headers.get("Location"))
}
// Return modified response
let response = new Response(original_text, {
status,
headers: new_response_headers
})
return response;
}
};
/**
* Handle HTTP requests
* @param {Request} req Request object
* @param {string} pathname Request path
*/
function httpHandler(req, pathname) {
const reqHdrRaw = req.headers
// Handle preflight requests
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)
}
/**
* Proxy request
* @param {URL} urlObj URL object
* @param {RequestInit} reqInit Request initialization object
* @param {string} rawLen Original length
*/
async function proxy(urlObj, reqInit, rawLen) {
const res = await fetch(urlObj.href, reqInit)
const resHdrOld = res.headers
const resHdrNew = new Headers(resHdrOld)
// Validate length
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')
// Remove unnecessary headers
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
})
}
async function ADD(envadd) {
var addtext = envadd.replace(/[ |"'\r\n]+/g, ',').replace(/,+/g, ','); // Replace spaces, double quotes, single quotes, and newlines with commas
//console.log(addtext);
if (addtext.charAt(0) == ',') addtext = addtext.slice(1);
if (addtext.charAt(addtext.length -1) == ',') addtext = addtext.slice(0, addtext.length - 1);
const add = addtext.split(',');
//console.log(add);
return add ;
}
- You can use existing open-source projects like
cloudflare-docker-proxy
, or write your own proxy logic. - Here, we take
cloudflare-docker-proxy
as an example and clone its source code from GitHub.
2. Modify Configuration#
- Find the configuration file in the source code (such as
config.js
), and modify the relevant configurations to suit your Docker repository and your domain name.
3. Deploy Source Code to Cloudflare Workers#
- In the Cloudflare Workers interface, create a new worker and paste or upload your source code.
- Configure your domain name and routing rules.
4. Test and Validate#
- Use your domain name along with the Docker image path, such as
<your-domain>/image-name:tag
, to pull the image. - Ensure everything is smooth and check the logs to diagnose any issues.
3. Notes#
- Ensure your Cloudflare Workers quota is sufficient, as this will consume request counts.
- Do not expose your domain name online to prevent being spammed.
This is the basic process of setting up a Docker image proxy using Cloudflare Workers. I hope this tutorial helps you! If you have any questions or need further guidance, please feel free to ask.