Logo

dev-resources.site

for different kinds of informations.

Use Cloudflare Snippets to set up a Docker Registry Mirror

Published at
12/27/2024
Categories
cloudflare
docker
Author
ccbikai
Categories
2 categories in total
cloudflare
open
docker
open
Author
7 person written this
ccbikai
open
Use Cloudflare Snippets to set up a Docker Registry Mirror

Using Cloudflare Workers to set up Docker image proxies works fine for personal use with low request volumes. However, if made public, high request volumes can incur significant costs.

Actually, Cloudflare has an even lighter JS Runtime called Cloudflare Snippets, though it comes with stricter limitations: 5ms CPU execution time, 2MB memory limit, and 32KB code size limit. Still, it's sufficient for request rewriting purposes.

Unfortunately, Cloudflare Snippets isn't currently available for Free plans, although their blog mentions that Free plans can create 5 Snippets.

If you have a Pro plan, you can slightly modify the Cloudflare Workers code to run it. It supports Docker Hub, Google Container Registry, GitHub Container Registry, Amazon Elastic Container Registry, Kubernetes Container Registry, Quay, and Cloudsmith.

Modified code:

// Raw Codes: https://github.com/ciiiii/cloudflare-docker-proxy/blob/master/src/index.js

const CUSTOM_DOMAIN = 'your.domains'
const MODE = 'production'

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

const routes = {
    // production
    [`docker.${CUSTOM_DOMAIN}`]: dockerHub,
    [`quay.${CUSTOM_DOMAIN}`]: 'https://quay.io',
    [`gcr.${CUSTOM_DOMAIN}`]: 'https://gcr.io',
    [`k8s-gcr.${CUSTOM_DOMAIN}`]: 'https://k8s.gcr.io',
    [`k8s.${CUSTOM_DOMAIN}`]: 'https://registry.k8s.io',
    [`ghcr.${CUSTOM_DOMAIN}`]: 'https://ghcr.io',
    [`cloudsmith.${CUSTOM_DOMAIN}`]: 'https://docker.cloudsmith.io',
    [`ecr.${CUSTOM_DOMAIN}`]: 'https://public.ecr.aws',

    // staging
    [`docker-staging.${CUSTOM_DOMAIN}`]: dockerHub,
}

async function handleRequest(request) {
    const url = new URL(request.url)
    const upstream = routeByHosts(url.hostname)
    if (upstream === '') {
        return new Response(
            JSON.stringify({
                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,
            redirect: 'follow',
        })
        if (resp.status === 401) {
            return responseUnauthorized(url)
        }
        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) {
            const 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',
    })
    const resp = await fetch(newReq)
    if (resp.status === 401) {
        return responseUnauthorized(url)
    }
    return resp
}

function routeByHosts(host) {
    if (host in routes) {
        return routes[host]
    }
    if (MODE === 'debug') {
        return dockerHub
    }
    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
    })
}

function responseUnauthorized(url) {
    const headers = new(Headers)()
    if (MODE === 'debug') {
        headers.set(
            'Www-Authenticate',
            `Bearer realm="http://${url.host}/v2/auth",service="cloudflare-docker-proxy"`,
        )
    } else {
        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,
    })
}

export default {
    fetch: handleRequest,
}

Enter fullscreen mode Exit fullscreen mode

stat

cloudflare Article's
30 articles in total
Favicon
Building a JAMStack App with Eleventy.js, CloudFlare Workers and AthenaHealth APIs - Part 2
Favicon
Building a JAMStack App with Eleventy.js, CloudFlare Workers and AthenaHealth APIs - Part 1
Favicon
How I Set Up My Custom Domain and Email for Substack
Favicon
Using Cloudflare SSL with Elastic Beanstalk instances
Favicon
Traefik using owned SSL certificate
Favicon
Traefik Cloudflare DNS Challenge
Favicon
Use Cloudflare Snippets to set up a Docker Registry Mirror
Favicon
Cloudflare PyPI Mirror
Favicon
Secure Self-Hosting with Cloudflare Tunnels and Docker: Zero Trust Security
Favicon
How to Process Incoming Emails and Trigger Webhooks, In-App Actions, and More Using Cloudflare Email Workers and D1 Database
Favicon
12 things I learned about hosting serverless sites on Cloudflare
Favicon
Dynamic DNS sync with Cloudflare
Favicon
Solusi Comment Reply WordPress Error Karena Rocket Loader Cloudflare
Favicon
Implementing Gmail Sending with Cloudflare Workers - Setup Guide
Favicon
Next.js Optimization for Dynamic Apps: Vercel Edge vs. Traditional SSR
Favicon
Building Vhisper: Voice Notes App with AI Transcription and Post-Processing
Favicon
Host Responded to 4 TCP SYN Probes on Port 24567 from Source Port 53(PCI DSS Cloudflare Resolved)
Favicon
Fighting with Redirects: A Journey of Astro Site Migration
Favicon
Using PostHog in Remix Loaders and Actions on Cloudflare Pages
Favicon
Implementing Gmail API with Cloudflare Workers - Part 3: Implementation
Favicon
Cloudflare Zaraz VS WP Complianz
Favicon
Implementing Gmail Sending with Cloudflare Workers - Development Guide
Favicon
Implementing Cloudflare Workflows
Favicon
Building Honcanator: The AI Goose Generator
Favicon
Retrieval Augmented Geese - Semantic Search with the HONC Stack
Favicon
Simplify serverless scaling and data management with Fauna and Cloudflare Workers
Favicon
[Cloudflare] Redirect To Another Domain
Favicon
Step-by-Step Guide to Hosting Your Website on a VPS Using Caddy Server and Cloudflare
Favicon
Building an AI Cron Builder with Cloudflare Pages and Next.js
Favicon
Cloudflare + Remix + PostgreSQL with Prisma Accelerate's Self Hosting

Featured ones: