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 DomainsAdd 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 R2Custom 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 routes array in wrangler.jsonc to 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_URL secret referenced by src/lib/r2.ts.
  • Secret Management: Use wrangler secret put locally or the secrets: block in .github/workflows/deploy.yml for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →