How to Configure Custom Domains for Cloudflare Workers Deployment in Next.js 15
To configure custom domains for Cloudflare Workers deployment, register your domain in the Cloudflare Dashboard, bind it via the routes array in wrangler.jsonc, and set the CLOUDFLARE_R2_URL secret to serve R2 assets from your branded URL.
This guide explains how to configure custom domains for Cloudflare Workers deployment using the ifindev/fullstack-next-cloudflare repository—a Next.js 15 application built for Cloudflare's edge runtime. By default, Wrangler deploys your worker to a *.workers.dev subdomain, but production environments require custom hostnames for both the application entry point and publicly accessible R2 storage.
Step 1: Register the Custom Domain in Cloudflare
Before modifying code, register your domain in the Cloudflare Dashboard. Navigate to Workers & Pages → select your worker → Custom Domains → Add Domain (e.g., app.my-domain.com). Cloudflare automatically generates a DNS CNAME record pointing to <worker-name>.<subdomain>.workers.dev. Add this record to your zone file and wait for DNS propagation, which typically completes within minutes.
Step 2: Bind Routes in wrangler.jsonc
The wrangler.jsonc configuration file controls how Cloudflare routes incoming requests to your worker. To bind your custom domain, add a routes array that matches your hostname pattern. According to the source code in ifindev/fullstack-next-cloudflare, this entry tells the edge network to invoke your worker for all requests hitting your custom domain.
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "next-cf-app",
"main": ".open-next/worker.js",
"compatibility_date": "2025-03-01",
"routes": [
"app.my-domain.com/*"
],
"assets": {
"binding": "ASSETS",
"directory": ".open-next/assets"
},
"r2_buckets": [
{
"bucket_name": "next-cf-app-bucket",
"binding": "next_cf_app_bucket"
}
]
}
The routes key supports multiple entries or wildcard patterns, allowing you to serve the worker from both app.my-domain.com/* and api.my-domain.com/* simultaneously.
Step 3: Configure R2 Custom Domains and Environment Secrets
Static assets stored in R2 require a separate custom domain configuration. In the Cloudflare Dashboard, navigate to R2 → Custom Domains and create a dedicated hostname (e.g., files.my-domain.com) for your bucket. Then, expose this domain to your application via the CLOUDFLARE_R2_URL secret.
In src/lib/r2.ts, the uploadToR2 function constructs public URLs by reading this secret from the environment at lines 49–50:
// src/lib/r2.ts
export async function uploadToR2(file: File, folder = "uploads"): Promise<UploadResult> {
const { env } = await getCloudflareContext();
const key = `${folder}/${Date.now()}_${Math.random().toString(36).substring(2)}.${file.name.split(".").pop()}`;
await env.next_cf_app_bucket.put(key, await file.arrayBuffer(), {
httpMetadata: { contentType: file.type }
});
// Uses the custom domain stored in CLOUDFLARE_R2_URL
const publicUrl = `https://${(env as any).CLOUDFLARE_R2_URL}/${key}`;
return { success: true, url: publicUrl, key };
}
Set the secret locally using Wrangler:
echo "files.my-domain.com" | wrangler secret put CLOUDFLARE_R2_URL
For production, the GitHub Actions workflow in .github/workflows/deploy.yml injects this secret automatically during deployment.
Step 4: Deploy via CLI or GitHub Actions
Once wrangler.jsonc and secrets are configured, deploy your worker. For local deployments, run:
pnpm run deploy
For continuous deployment, the repository includes a GitHub Actions workflow that pushes the worker and injects the required secrets. The Deploy to Production job passes CLOUDFLARE_R2_URL along with authentication credentials:
# .github/workflows/deploy.yml
- name: Deploy to Production
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
secrets: |
BETTER_AUTH_SECRET
GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET
CLOUDFLARE_R2_URL
After deployment, verify connectivity by curling your custom domain endpoint (e.g., curl -f https://app.my-domain.com/api/todos).
Summary
- Dashboard Configuration: Register the custom domain under Workers → Custom Domains and add the provided CNAME record to your DNS zone.
- Route Binding: Add your hostname to the
routesarray inwrangler.jsoncto map incoming requests to the worker. - R2 Asset Domain: Configure a separate custom domain for R2 in the Cloudflare Dashboard, then store it in the
CLOUDFLARE_R2_URLsecret referenced bysrc/lib/r2.ts. - Secret Management: Use
wrangler secret putlocally or thesecrets:block in.github/workflows/deploy.ymlfor CI/CD injection. - Verification: Confirm deployment success by accessing both the application root and uploaded R2 assets via your custom URLs.
Frequently Asked Questions
How long does DNS propagation take after adding a custom domain?
DNS propagation typically completes within five minutes when using Cloudflare's nameservers, though it can take up to 24 hours depending on TTL settings and regional DNS caches. Verify propagation by running dig app.my-domain.com and confirming the CNAME points to your workers.dev subdomain.
Can I use multiple custom domains for the same Cloudflare Worker?
Yes. The routes field in wrangler.jsonc accepts an array of patterns, allowing you to bind multiple hostnames (e.g., app.my-domain.com/* and www.my-domain.com/*) to a single worker deployment. Each domain must be registered individually in the Cloudflare Dashboard.
Why does my R2 bucket need a separate custom domain from the worker?
R2 custom domains provide a dedicated hostname for object storage (e.g., files.my-domain.com), separating asset delivery from application logic. This isolation improves cacheability, allows direct browser access to objects, and prevents CORS issues when loading media from a different origin than your Next.js app.
Where should I store the CLOUDFLARE_R2_URL secret for local development?
For local development, store the secret using wrangler secret put CLOUDFLARE_R2_URL or define it in a .dev.vars file in your project root. The getCloudflareContext function used in src/lib/r2.ts reads from these local secrets during wrangler dev sessions, mimicking the production environment where the value is injected by the GitHub Actions workflow.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →