Self-hosted service
cloudflare/serverless-registry avatar
cloudflare/serverless-registry

Cloudflare serverless-registry: Docker Registry on Workers and R2

A container registry backed by Workers and R2.

1,468 stars126 forksTypeScriptApache-2.0

At a glance

What is it?
Cloudflare serverless-registry implements a Docker-compatible container registry in a Worker, using R2 for image data and supporting password or public-key JWT authentication. It suits small to medium image sets that fit the documented Workers request limits, but its trust and storage model requires careful deployment planning.
Who is it for?
Adopt serverless-registry when your images fit the documented upload constraints and you want registry behavior backed entirely by Cloudflare resources. Do not choose it when you need per-repository authorization, multi-account token isolation, or unrestricted large-layer uploads.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 42 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why Cloudflare serverless-registry exists

Cloudflare serverless-registry targets teams that want a Docker-compatible image registry without operating a separate registry application and its persistent storage tier. The repository describes an implementation in Cloudflare Workers with image data stored in R2. Its intended users are engineers and platform teams that already use Docker images, need to push and pull them through a registry endpoint, and are willing to design authentication and request-size constraints around the Workers environment.

The project supports the standard push and pull workflows described in the README. It also offers two authentication models: `USERNAME` and `PASSWORD` credentials, and tokens verified through a base64-encoded public key configured as `JWT_REGISTRY_TOKENS_PUBLIC_KEY`. The registry refuses requests when no credentials have been configured, which makes deployment incomplete until the operator selects and provisions an authentication method.

This is not presented as a general-purpose multi-tenant registry with fine-grained permissions. The README explicitly says there is no per-account or per-repository scoping. A valid token grants capabilities across the entire registry. That makes the design more appropriate for an organization-controlled service with a clear trust boundary than for a public service in which unrelated users need isolated repository access.

Workers, R2, and Docker request flow

The central architecture is compact: a Cloudflare Worker handles registry requests, while an R2 bucket holds registry data through the `REGISTRY` binding. The `package.json` identifies `r2-registry` version `1.1.0` as an ES module TypeScript project. Its routing dependency is `itty-router`, validation is handled with `zod`, and JWT verification uses `@tsndr/cloudflare-worker-jwt`. Those dependencies support the README's claims about registry routing, validation, and public-key token verification, although the repository documentation does not describe every route or internal storage key.

Docker remains the external client interface. After a container image is tagged with the registry host, `docker push` sends the image workflow to the Worker and `docker pull` retrieves it. With pull fallback enabled, an object missing from R2 is downloaded from another registry and copied into the R2 bucket. A later pull can then be served directly from R2. This behavior makes the project useful during a staged migration because existing image names can be retrieved through one endpoint before the corresponding layers have all been copied locally.

The fallback path supports Basic and Bearer authentication as defined by the registry specification referenced in the README. Authentication credentials for the upstream registry are represented through configuration, while secrets are entered with `npx wrangler secret put`. The documented first-use model therefore has two possible storage paths: direct Worker-to-R2 operation for content already held by the registry, or an upstream fetch followed by R2 population when fallback is configured.

Installing and deploying the Worker

The repository requires Node.js `>=24.0.0` and pnpm `>=11.0.0` in `package.json`, with `[email protected]` recorded as its package manager. pnpm is the only supported package manager according to the README. Install the locked dependencies from the repository root with the documented command:

bash
$ pnpm install

The README recommends creating a local Wrangler configuration from one of the supplied examples. It supports both JSONC and TOML, so choose one rather than copying both:

bash
# JSONC (recommended)
$ cp wrangler.example.jsonc wrangler.jsonc

# or TOML
$ cp wrangler.example.toml wrangler.toml

Create the R2 bucket used by the registry. The command below uses the same bucket name that appears in the configuration examples:

bash
$ npx wrangler --env production r2 bucket create r2-registry

The Worker configuration must bind that bucket to `REGISTRY`. For a JSONC configuration, add the binding shown in the README:

jsonc
// wrangler.jsonc
"r2_buckets": [
  { "binding": "REGISTRY", "bucket_name": "r2-registry" }
]

The documented deployment command uses the `production` environment:

bash
$ npx wrangler deploy --env production

At this point, the README says the registry is running, but it will reject requests until credentials are configured. The repository also provides `pnpm dev:miniflare`, which starts Wrangler on port `9999` with the `dev` environment and live reload. The README warns that Docker pushes to this local development path require the Worker to buffer the request.

Authenticating Docker clients and verifying tokens

For username and password authentication, provision both values as production secrets. The project does not document these credentials in the Wrangler configuration file:

bash
npx wrangler secret put USERNAME --env production
npx wrangler secret put PASSWORD --env production

The README also supports public-key JWT authentication for passwords or tokens signed by the corresponding private key. Store the base64-encoded public key under the documented secret name:

bash
npx wrangler secret put JWT_REGISTRY_TOKENS_PUBLIC_KEY --env production

JWT validation includes an audience constraint. Every token must contain an `aud` claim naming its registry, and a request receives `401` when that value does not match the request host. The documentation says only host and port are compared. As a result, `https://registry.example`, `http://registry.example`, and `registry.example` are equivalent for this check, while `registry.example:8787` is a separate registry identity.

The scope is wider than the name might suggest. The README states that every deployment should have its own key pair because tokens are not limited by account or repository. Deployments that share `JWT_REGISTRY_TOKENS_PUBLIC_KEY` form one trust domain. Token generation through `createToken()` derives `aud` from its `registryUrl` argument, but the audience check is the only stated separation between deployments sharing the key.

First Docker pull, tag, and push workflow

Set `REGISTRY_URL` to the deployed registry address, then authenticate Docker with the configured username and password. Passing the password through standard input avoids placing it directly in the command arguments:

bash
export REGISTRY_URL=your-url-here

# Replace $PASSWORD and $USERNAME with the actual credentials
echo $PASSWORD | docker login --username $USERNAME --password-stdin $REGISTRY_URL
docker pull ubuntu:latest
docker tag ubuntu:latest $REGISTRY_URL/ubuntu:latest
docker push $REGISTRY_URL/ubuntu:latest

The README then verifies a pull after removing both the original and registry-tagged local images:

bash
docker rmi ubuntu:latest $REGISTRY_URL/ubuntu:latest
docker pull $REGISTRY_URL/ubuntu:latest

This example assumes the registry is configured to permit the requested operation and that the image layers meet the size constraints described in the documentation. The `ubuntu:latest` tag is illustrative. The README does not define image retention policies, garbage collection, deletion semantics, or production hostname provisioning, so an operator should not infer those behaviors from the push and pull example.

Pull fallback for staged registry migration

Pull fallback lets the Worker treat another registry as an upstream source. If a requested image is not present in R2, the registry downloads it from the configured source and copies it into R2. This is materially different from requiring a migration job to copy every image first. It also means that the first request for a missing image can depend on both the Worker and the upstream registry.

The upstream registry, credential environment variable, and optional username are encoded in the `REGISTRIES_JSON` value. This JSONC example defines the required shape:

jsonc
// wrangler.jsonc
"env": {
  "production": {
    "vars": {
      "REGISTRIES_JSON": "[{ \"registry\": \"https://url-to-other-registry\", \"password_env\": \"REGISTRY_TOKEN\", \"username\": \"username-to-use\" }]"
    }
  }
}

The actual upstream token must be stored as a secret, not copied into the configuration. The README gives this command pattern for a credential already exposed through an environment variable:

bash
echo $GITHUB_TOKEN | npx wrangler secret put REGISTRY_TOKEN --env production

Anonymous Docker Hub fallback is also documented:

jsonc
// wrangler.jsonc
"REGISTRIES_JSON": "[{ \"registry\": \"https://index.docker.io/\" }]"

The README notes that Docker Hub credentials can be configured to avoid anonymous rate limits, but it does not document a rate-limit guarantee. A deployment that depends heavily on fallback should therefore account for upstream availability and authentication behavior as part of registry operation.

Size limits, operations, and maintenance boundaries

The most concrete constraint is the documented 500 MB maximum layer size for Docker pushes. The README attributes this to maximum request body sizes in Workers plans. Larger uploads cannot simply be treated as an ordinary `docker push` workflow. It points to two possible workarounds: interacting with the R2 bucket manually, or examining the `./push` directory for guidance on pushing large layers. Neither workaround is presented as a fully documented replacement for an oversized Docker request.

Local development has a separate operational caveat. With `npx wrangler dev`, a Docker push makes the R2 registry buffer the request in the Worker. That development path should not be confused with the deployed production architecture or treated as evidence about production buffering behavior.

The repository is not archived, and its last push was on 2026-09-11. That indicates recent repository activity, but it has no GitHub releases. `package.json` reports version `1.1.0`, while the README does not publish a release history or upgrade procedure. There is also no documented migration note for changes to R2 bindings, authentication secrets, or `REGISTRIES_JSON`.

The project uses the `Apache-2.0` license. The repository includes a `LICENSE` file and identifies the same license in `package.json`. That is useful for source reuse and modification under the license terms, but this review does not interpret those terms as legal advice.

Why a conventional registry remains a different choice

The real alternative is to continue using a conventional container registry with its own registry server and storage system, or to make that registry the direct destination for Docker clients. The architectural difference is straightforward. A conventional registry keeps the registry application and image storage behind a dedicated service. Cloudflare serverless-registry instead maps Docker traffic to a Worker and stores data in R2, which can reduce the need to operate a separate registry service but ties request handling and uploads to Cloudflare platform constraints.

Pull fallback does not make the two approaches identical. It allows serverless-registry to consult an upstream registry and copy missing content into R2, but the README does not claim continuous background replication or synchronized deletion. Once content has been copied, later requests can use R2 directly. That is useful for staged migration, yet operators should not assume the fallback registry is merely a transparent proxy forever.

The choice therefore turns on authorization and workload boundaries. serverless-registry is a credible option when one deployment has a controlled user group, its token audience is managed carefully, and each image layer is within the documented limit. A conventional registry is the clearer choice when repository isolation, a mature operational model, or upload patterns outside the Worker request boundary are primary requirements.

Editorial conclusion

Adopt serverless-registry when your images fit the documented upload constraints and you want registry behavior backed entirely by Cloudflare resources. Do not choose it when you need per-repository authorization, multi-account token isolation, or unrestricted large-layer uploads. Verify your largest compressed layer, the configured authentication method, the intended `aud` value, and the secret-management process before production use. For images within those boundaries, the documented Worker and R2 architecture is a direct fit for Docker push and pull workflows.

Frequently asked questions

How do I deploy Cloudflare serverless-registry?

Install dependencies with `pnpm install`, copy either `wrangler.example.jsonc` or `wrangler.example.toml`, create the `r2-registry` bucket, bind it as `REGISTRY`, and run `npx wrangler deploy --env production`. The README says the registry rejects requests until credentials are configured.

Which authentication methods does Cloudflare serverless-registry support?

It supports `USERNAME` and `PASSWORD` authentication, plus public-key JWT authentication through `JWT_REGISTRY_TOKENS_PUBLIC_KEY`. JWT requests must carry an `aud` claim that matches the registry host and port.

What is the maximum Docker layer size supported by Cloudflare serverless-registry?

The README states that Docker pushes are limited to images with a maximum layer size of 500 MB. It associates this restriction with Workers request body limits and points to manual R2 uploads or the `./push` directory for larger-layer approaches.

Can Cloudflare serverless-registry pull images from another registry?

Yes, when `REGISTRIES_JSON` defines an upstream registry. The registry downloads missing content from that source and copies it into R2, where later pulls can retrieve it directly. Basic and Bearer upstream authentication are supported.

Official sources

  1. cloudflare/serverless-registry on GitHub
  2. Issues
  3. License: Apache-2.0
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/cloudflare-serverless-registry.svg)](https://hysenlabs.com/projects/cloudflare-serverless-registry)

Was this useful?