Alibaba’s image accelerator has recently stopped working, so another route is needed. If you already have a domain and use Cloudflare for DNS, you can deploy a small Cloudflare Workers proxy and use a subdomain as a Docker registry mirror.

The setup requires:

  • A domain name
  • The domain’s nameservers changed to Cloudflare

Once configured, a Docker image can be pulled through a custom endpoint such as https://docker.krjojo.com:

docker pull docker.krjojo.com/library/mysql:8.0

Private image mirrors are not necessarily stable or trustworthy, including the example above. Use them for testing at your own risk. The subdomain does not have to be named docker; it can be changed to any suitable name.

Deploying a Worker manually

Log in to the Cloudflare dashboard, open Workers & Pages from the left navigation, and choose Create on the right.

Cloudflare Workers dashboard

Choose a Worker, enter a name, and deploy it. Naming it docker makes it easier to identify later.

Creating a Worker Worker creation options Naming the Worker

After deployment succeeds, select Edit code, replace the existing contents with the following script, change workers_url to your own domain, and click Deploy in the upper-right corner.

'use strict'

const hub_host = 'registry-1.docker.io'
const auth_url = 'https://auth.docker.io'
const workers_url = 'https://你的域名'
/**
 * static files (404.html, sw.js, conf.js)
 */

/** @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 === '/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
    })
}

Worker code editor

Return to Workers & Pages, open the Worker’s settings, and add your domain under Domains & Routes. Cloudflare will handle HTTPS automatically.

The workers.dev subdomain provided by Cloudflare can be removed. In some cases it is difficult to access, and if it does not match the domain written into workers_url, it will not work properly.

Adding a custom domain

Finally, configure Docker to use the mirror:

{
  "registry-mirrors": ["https://docker.krjojo.com"]
}

An alternative based on cloudflare-docker-proxy

There is an important limitation with the manual Worker script: Docker also needs to reach https://auth.docker.io to obtain an anonymous token. If that endpoint is blocked or unreachable, image metadata resolution fails with an error similar to this:

 [internal] load metadata for docker.io/library/php:8.3.8-fpm-alpine
failed to solve: DeadlineExceeded: DeadlineExceeded: DeadlineExceeded: php:8.3.8-fpm-alpine: failed to resolve source metadata for docker.io/library/php:8.3.8-fpm-alpine: failed to authorize: DeadlineExceeded: failed to fetch anonymous token: Get "https://auth.docker.io/token?scope=repository%3Alibrary%2Fphp%3Apull&service=registry.docker.io": dial tcp 174.37.175.229:443: i/o timeout
#2 ERROR: failed to authorize: DeadlineExceeded: failed to fetch anonymous token: Get "https://auth.docker.io/token?scope=repository%3Alibrary%2Fphp%3Apull&service=registry.docker.io": dial tcp 174.37.175.229:443: i/o timeout
------
> [internal] load metadata for docker.io/library/php:8.3.8-fpm-alpine:

Because of this dependency, the simple approach may not solve every connectivity problem. Another option is to fork and deploy the cloudflare-docker-proxy project with your own domain. The project’s original domain may itself be contaminated or inaccessible, so the domain mappings should be changed before deployment.

Fork the project first, then edit src/index.js. Replace every occurrence of libcuda.so with your own root domain, without a subdomain prefix. For example, use krjojo.com rather than docker.krjojo.com:

const routes = {
  // production
  "docker.libcuda.so": dockerHub,
  "quay.libcuda.so": "https://quay.io",
  "gcr.libcuda.so": "https://gcr.io",
  "k8s-gcr.libcuda.so": "https://k8s.gcr.io",
  "k8s.libcuda.so": "https://registry.k8s.io",
  "ghcr.libcuda.so": "https://ghcr.io",
  "cloudsmith.libcuda.so": "https://docker.cloudsmith.io",
  "ecr.libcuda.so": "https://public.ecr.aws",

  // staging
  "docker-staging.libcuda.so": dockerHub,
};

For example, the edited route table can look like this:

const routes = {
  // production
  "docker.krjojo.com": dockerHub,
  "quay.krjojo.com": "https://quay.io",
  "gcr.krjojo.com": "https://gcr.io",
  "k8s-gcr.krjojo.com": "https://k8s.gcr.io",
  "k8s.krjojo.com": "https://registry.k8s.io",
  "ghcr.krjojo.com": "https://ghcr.io",
  "cloudsmith.krjojo.com": "https://docker.cloudsmith.io",
  "ecr.krjojo.com": "https://public.ecr.aws",

  // staging
  "docker-staging.krjojo.com": dockerHub,
};

You can also edit README.md and replace ciiiii with your own GitHub username. This makes the Deploy to Cloudflare Workers button point to your fork. If you skip this change, the deployment link can still be adjusted manually.

# cloudflare-docker-proxy

![deploy](/images/51fde6d65d981c06d53e5c53777b14f3b28be5c646b92c79a6d941feaf5590c9.svg)

[![Deploy to Cloudflare Workers](/images/53f64b79021eb8d22f1fdd23fb3752b132915da4f953d32dd31201c87fe488dc.png)](https://deploy.workers.cloudflare.com/?url=https://github.com/ciiiii/cloudflare-docker-proxy)

> If you're looking for proxy for helm, maybe you can try [cloudflare-helm-proxy](https://github.com/ciiiii/cloudflare-helm-proxy).

## Deploy

1. click the "Deploy With Workers" button
2. follow the instructions to fork and deploy
3. update routes as you requirement

[![Deploy to Cloudflare Workers](/images/53f64b79021eb8d22f1fdd23fb3752b132915da4f953d32dd31201c87fe488dc.png)](https://deploy.workers.cloudflare.com/?url=https://github.com/ciiiii/cloudflare-docker-proxy)

The edited README uses the fork owner in the deployment URL:

# cloudflare-docker-proxy

![deploy](/images/51fde6d65d981c06d53e5c53777b14f3b28be5c646b92c79a6d941feaf5590c9.svg)

[![Deploy to Cloudflare Workers](/images/53f64b79021eb8d22f1fdd23fb3752b132915da4f953d32dd31201c87fe488dc.png)](https://deploy.workers.cloudflare.com/?url=https://github.com/miniwater/cloudflare-docker-proxy)

> If you're looking for proxy for helm, maybe you can try [cloudflare-helm-proxy](https://github.com/ciiiii/cloudflare-helm-proxy).

## Deploy

1. click the "Deploy With Workers" button
2. follow the instructions to fork and deploy
3. update routes as you requirement

[![Deploy to Cloudflare Workers](/images/53f64b79021eb8d22f1fdd23fb3752b132915da4f953d32dd31201c87fe488dc.png)](https://deploy.workers.cloudflare.com/?url=https://github.com/miniwater/cloudflare-docker-proxy)

Open your fork’s project page and click Deploy with Workers. Cloudflare will guide you through authorization and account linking.

During authorization, enter the account ID on the left. You can find it in the Cloudflare dashboard by opening your domain and locating the API section on the lower-right side. On the right, enter an API token. Under the same API section, choose Get your API token, create a token from the Edit Cloudflare Workers template, select your own account as the account resource, and restrict the zone resource to the specific zone containing your domain.

Continue through the setup and activate GitHub Actions. After deployment, open Worker dash in the center, select cloudflare-docker-proxy, then go to Settings → Triggers. Add a custom domain such as docker.your-domain.com; for example, docker.krjojo.com.

If Docker is the only registry you need, the setup is complete at this point. The project can also proxy several other registries by adding the corresponding custom domains:

<table> <thead> <tr> <th>Custom domain</th> <th>Target</th> </tr> </thead> <tbody> <tr> <td>docker.krjojo.com</td> <td>https://registry-1.docker.io</td> </tr> <tr> <td>quay.krjojo.com</td> <td>https://quay.io</td> </tr> <tr> <td>gcr.krjojo.com</td> <td>https://gcr.io</td> </tr> <tr> <td>k8s-gcr.krjojo.com</td> <td>https://k8s.gcr.io</td> </tr> <tr> <td>k8s.krjojo.com</td> <td>https://registry.k8s.io</td> </tr> <tr> <td>ghcr.krjojo.com</td> <td>https://ghcr.io</td> </tr> <tr> <td>cloudsmith.krjojo.com</td> <td>https://docker.cloudsmith.io</td> </tr> <tr> <td>ecr.krjojo.com</td> <td>https://public.ecr.aws</td> </tr> <tr> <td>docker-staging.krjojo.com</td> <td>https://registry-1.docker.io</td> </tr> </tbody> </table>

Configure Docker to use the Docker Hub endpoint as its registry mirror:

{
    "registry-mirrors": [
        "https://docker.krjojo.com"
    ]
}