Workers R2 -- Public Buckets and Custom Domains
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
- What is the advantage of using a public R2 bucket over proxying through a Worker?
- Which Wrangler command links a custom domain to an R2 bucket?
- What DNS requirement must the custom domain satisfy for R2 bucket linking?
FAQ
{{< 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