Skip to content

Workers R2 -- Public Buckets and Custom Domains

DodaTech 5 min read

In this tutorial, you will learn how to make Cloudflare R2 buckets publicly accessible over the internet and attach custom domains so users access objects through your own hostname rather than an R2 endpoint. Public buckets are important because they turn R2 into a static asset server for images, videos, and downloads without needing a Worker to proxy every request. A real-world example is a documentation site serving images and PDFs from an R2 bucket accessed at assets.docs.example.com.

Public Access vs Worker Proxying

By default, R2 buckets are private. To serve objects directly to browsers, you can either configure a public bucket with a custom domain or proxy requests through a Worker. Public buckets are simpler and more cost-effective for static content because R2 handles the request routing without consuming Worker CPU time. Worker proxying gives you access control, request transformation, and authentication in exchange for additional compute cost and latency.

Public Bucket Architecture

flowchart LR
    B[Browser] --> D[Custom Domain cdn.example.com]
    D --> P[Cloudflare Proxy]
    P --> R[R2 Bucket public-bucket]
    R --> O[Object returned directly]

    subgraph Direct Access
        B2[Browser] --> R2[R2 Bucket via r2.dev domain]
        R2 --> O2
    end

    style D fill:#f90,color:#fff
    style R fill:#3498db,color:#fff
    style R2 fill:#95a5a6,color:#fff

When you connect a custom domain to an R2 bucket, Cloudflare automatically routes requests for that domain to the bucket. Objects are served with their original content types and cache headers. Cloudflare's CDN caches responses at the edge, so subsequent requests are served from cache.

Enabling a Public Bucket

# Create a bucket for public assets
npx wrangler r2 bucket create public-assets

# Expected output:
# Creating bucket public-assets...
# Bucket created successfully

Then navigate to the Cloudflare Dashboard under R2, select your bucket, and enable public access. The bucket receives a generated r2.dev domain for testing.

// Worker-based gate for a mixed public/private bucket
export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    // Public assets served directly from R2 via custom domain
    if (url.pathname.startsWith('/public/')) {
      return fetch('https://public-assets.example.com' + url.pathname);
    }

    // Private assets require authentication
    if (url.pathname.startsWith('/private/')) {
      const auth = request.headers.get('Authorization');
      if (!auth) return new Response('Unauthorized', { status: 401 });
      // Validate token, then proxy from R2 via Worker binding
      const object = await env.BUCKET.get(url.pathname);
      if (!object) return new Response('Not found', { status: 404 });
      const headers = new Headers();
      object.writeHttpMetadata(headers);
      return new Response(object.body, { headers });
    }

    return new Response('Not found', { status: 404 });
  }
};

This Worker routes /public/* requests to the public bucket's custom domain and serves /private/* through the Worker binding with authentication.

Connecting a Custom Domain

# Use Wrangler to link a custom domain to an R2 bucket
npx wrangler r2 bucket domain add public-assets cdn.example.com

# Expected output:
# Adding domain cdn.example.com to bucket public-assets...
# Domain linked successfully. DNS record created.

Your domain must be on Cloudflare's network (orange-clouded). Wrangler creates a DNS CNAME record automatically. Once linked, https://cdn.example.com/image.png serves the object with key image.png from the bucket.

Cache Configuration for Public Buckets

// Set cache headers on objects to control CDN behavior
export default {
  async fetch(request, env) {
    // When uploading, set Cache-Control headers
    const object = await env.BUCKET.get('images/banner.jpg');

    if (!object) return new Response('Not found', { status: 404 });

    const headers = new Headers();
    object.writeHttpMetadata(headers);
    // Override with longer cache for public bucket assets
    headers.set('Cache-Control', 'public, max-age=31536000, immutable');

    return new Response(object.body, { headers });
  }
};

// Expected response headers:
// Content-Type: image/jpeg
// Cache-Control: public, max-age=31536000, immutable
// ETag: "abc123"

For public buckets, set cache headers at upload time via the httpMetadata parameter of put(). Cloudflare's CDN respects these headers for edge Caching. Immutable assets (versioned filenames) should use immutable in Cache-Control.

Common Errors and Troubleshooting

Domain Not on Cloudflare

The custom domain must be proxied through Cloudflare (orange cloud in DNS). If the domain is not on Cloudflare, Wrangler returns an error. Add the domain to Cloudflare first, then link it to the bucket.

DNS Propagation Delay

After linking a domain, DNS may take a few minutes to propagate worldwide. During this time, requests may return 522 errors. Wait 5-10 minutes and verify with dig cdn.example.com.

403 Forbidden on Public Bucket

If a public bucket returns 403, the bucket's public access setting may be disabled. Verify in the Cloudflare Dashboard under R2 - Bucket - Settings that public access is enabled.

Custom Domain Without HTTPS

Cloudflare automatically provisions an SSL certificate for the custom domain via Universal SSL or a custom certificate. HTTPS is always enforced for R2 bucket domains.

Object Not Found on Custom Domain

The R2 bucket custom domain maps the request path directly to the object key. A request to cdn.example.com/images/logo.png expects an object with key images/logo.png. Ensure the object key matches the URL path exactly.

Practice Questions

  1. What is the advantage of using a public R2 bucket over proxying through a Worker?
  2. Which Wrangler command links a custom domain to an R2 bucket?
  3. What DNS requirement must the custom domain satisfy for R2 bucket linking?

FAQ

Can I use a public R2 bucket without a custom domain?

Yes. Every public bucket receives a generated r2.dev subdomain (e.g., bucket-name.r2.dev) that you can use for testing and development. For production, use a custom domain to maintain brand consistency and avoid the r2.dev domain in URLs.

Does a public bucket support directory indexes?

No. R2 public buckets serve individual objects by exact key. There is no automatic index page for directory paths. Requesting cdn.example.com/images/ without a specific file returns a 404. For index pages, use a Worker to generate HTML listings or serve an index.html object paired with a redirect rule.

{{< faq "What Caching headers does R2 set by default?">}} R2 does not set default Cache-Control headers. Objects are served with whatever headers were provided at upload time. If no cache headers are set, Cloudflare's CDN may use default cache TTLs. Always set explicit Cache-Control headers on uploads for predictable Caching behavior.{{< /faq >}}

Summary

R2 public buckets with custom domains provide direct object access through your own hostname with zero egress fees and automatic CDN Caching. Enable public access in the dashboard, link a Cloudflare-proxied domain with Wrangler or the dashboard, and serve assets at scale. For mixed public and private content, combine a public bucket with a Worker that handles authentication for restricted paths.

This guide is brought to you by the developers of Cloudflare, Cloudflare R2, and Durga Antivirus Pro at DodaTech.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro