cloudflare-docker-proxy icon indicating copy to clipboard operation
cloudflare-docker-proxy copied to clipboard

部署出问题的朋友们,看这里

Open jia-niang opened this issue 5 months ago • 6 comments

项目一段时间没维护,现在无法部署成功了。 不用想着怎么解决部署问题了,源码就是一个单文件,逻辑都很简单,我按照最新的 Workers 规范改了一下,直接使用即可。

新建一个 Workers,选 Hello World 项目,创建完成后点右上角的按钮进入编辑代码,填入以下代码:

const customDomain = '<换成你自己的域名>'

const dockerHub = 'https://registry-1.docker.io'
const routes = {
  // ↓ 这一行是我加的,用不到可以去掉
  [customDomain]: dockerHub,
  ['docker.' + customDomain]: dockerHub,
  ['quay.' + customDomain]: 'https://quay.io',
  ['gcr.' + customDomain]: 'https://gcr.io',
  ['k8s-gcr.' + customDomain]: 'https://k8s.gcr.io',
  ['k8s.' + customDomain]: 'https://registry.k8s.io',
  ['ghcr.' + customDomain]: 'https://ghcr.io',
  ['cloudsmith.' + customDomain]: 'https://docker.cloudsmith.io',
  ['ecr.' + customDomain]: 'https://public.ecr.aws',
  ['docker-staging.' + customDomain]: dockerHub,
}

export default {
  async fetch(request) {
    const url = new URL(request.url)

    if (url.pathname == '/') {
      return Response.redirect(url.protocol + '//' + url.host + '/v2/', 301)
    }

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

      const resp = await fetch(newUrl.toString(), {
        method: 'GET',
        headers: headers,
        redirect: 'follow',
      })

      if (resp.status === 401) {
        return responseUnauthorized(url)
      }

      return resp
    }

    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,
      // don't follow redirect to dockerhub blob upstream
      redirect: isDockerHub ? 'manual' : 'follow',
    })

    const resp = await fetch(newReq)
    if (resp.status == 401) {
      return responseUnauthorized(url)
    }

    // handle dockerhub blob redirect manually
    if (isDockerHub && resp.status == 307) {
      const location = new URL(resp.headers.get('Location'))
      const redirectResp = await fetch(location.toString(), {
        method: 'GET',
        redirect: 'follow',
      })
      return redirectResp
    }

    return resp
  },
}

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

  return ''
}

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

function responseUnauthorized(url) {
  const headers = new Headers()

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

记得把代码第一行的变量替换成自己的域名,然后点击 “部署” 即可。 注意 Cloudflare 送的 *.workers.dev 在国内被 DNS 污染了,直接用是不行的,必须得绑定自己的域名。

想要测试是否部署成功,也很简单,在自己的电脑上运行:

docker pull <你自己的域名>/alpine

# 例如
docker pull example.com/alpine

试试镜像能不能拉下来就行,这里的域名不要带 https 前缀。

jia-niang avatar Jul 27 '25 18:07 jia-niang

即使绑定了自己的域名,好像因为 CNAME 的地址是 Cloudflare 的,访问还是有点不畅。 我在家里 docker pull 自己的镜像,会有失败的情况,多试几次能成功;在香港的云服务器拉自己的镜像,每次都能成功。

jia-niang avatar Jul 27 '25 18:07 jia-niang

wrangler 升級最新 4.26.0 CI Publish 前面加 - run: npm ci 也不是不行

xlionjuan avatar Jul 28 '25 14:07 xlionjuan

wrangler 升級最新 4.26.0 CI Publish 前面加 - run: npm ci 也不是不行

好吧,我就试了下升级 wrangler 加了环境变量,还是有问题,干脆用源码了。 可以给他提个 PR。

jia-niang avatar Jul 28 '25 14:07 jia-niang

不知為何我遇不到問題

xlionjuan avatar Jul 28 '25 18:07 xlionjuan

现在提示404

...
Using default tag: latest
Error response from daemon: error parsing HTTP 404 response body: invalid character '<' looking for beginning of value: "<!DOCTYPE html><html lang=\"en\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1...(后面跟了一个完整的404html内容)

kamjin3086 avatar Nov 19 '25 04:11 kamjin3086

现在提示404

...
Using default tag: latest
Error response from daemon: error parsing HTTP 404 response body: invalid character '<' looking for beginning of value: "<!DOCTYPE html><html lang=\"en\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1...(后面跟了一个完整的404html内容)

今天 Cloudflare 出了个全球网络故障问题,现在应该解决了,你再试一下看看

jia-niang avatar Nov 19 '25 10:11 jia-niang