DEV Community

Tias
Tias

Posted on

Cloudflare Workers tutorial with custom domain

So I spent too long figuring this out. After implementing the Caching recommendations from the OpenNext docs, I hit a cryptic error, which you'll read about below, and then I found the answer (thanks to a random Discord comment). I'm writing this article so you can just get it working too.

Goals:

  • Use @opennextjs/cloudflare.
  • Deploy via GitHub Actions.
  • Connect to a custom domain.
  • Use a monorepo.

We started with the standard command:

pnpm create cloudflare@latest example.com --framework=next
Enter fullscreen mode Exit fullscreen mode

Note: the OpenNext page uses a --platform=workers arg that apparently doesn't do anything (I diffed it). The Cloudflare page omits it.

That command generates a bunch of files and I'm going to assume that you ran that. I'm not going to print every file here.

The one important thing is that my project is a monorepo, with a web directory for the website workers project. You'll see this web path in the GitHub actions job.

wrangler.jsonc

Here is the wrangler config.

{
  "$schema": "node_modules/wrangler/config-schema.json",
  "name": "web",
  "main": ".open-next/worker.js",
  "compatibility_date": "2025-07-20",
  "compatibility_flags": [
    "nodejs_compat",
    "global_fetch_strictly_public",
  ],
  "observability": {
    "enabled": true,
  },
  "routes": [
    {
      "pattern": "example.com",
      "custom_domain": true,
    },
  ],
  "assets": {
    "binding": "ASSETS",
    "directory": ".open-next/assets",
  },
  "services": [
    {
      "binding": "WORKER_SELF_REFERENCE",
      "service": "web",
    },
  ],
  "r2_buckets": [
    {
      "binding": "NEXT_INC_CACHE_R2_BUCKET",
      "bucket_name": "next-inc-cache",
    },
  ],
  "durable_objects": {
    "bindings": [
      {
        "name": "NEXT_CACHE_DO_QUEUE",
        "class_name": "DOQueueHandler",
      },
    ],
  },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["DOQueueHandler"],
    },
  ],
  "d1_databases": [
    {
      "binding": "NEXT_TAG_CACHE_D1",
      "database_id": "______________",
      "database_name": "next-tag-cache",
    },
  ],
  "send_metrics": false,
}
Enter fullscreen mode Exit fullscreen mode

The routes block connects it to my custom domain (which is not example.com, opsec or whatever). I don't think you need to set up the custom domain in the dashboard manually, but you do definitely need to have the domain set up in the dashboard as a zone. You can't use someone else's domain, sorry.

My domain didn't have any records, and cloudflare sets up the records automatically when it creates the "custom domain" for the worker.

I wasn't sure if WORKER_SELF_REFERENCE was required, but it shouldn't hurt.

You do need to manually create an R2 bucket named next-inc-cache.

Also, create a D1 database named next-tag-cache and paste its id in database_id.

Yeah I disabled metrics. Mostly for maximal performance... Cloudflare should understand.

open-next.config.ts

import { defineCloudflareConfig } from '@opennextjs/cloudflare';
import r2IncrementalCache from '@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache';
import d1NextTagCache from '@opennextjs/cloudflare/overrides/tag-cache/d1-next-tag-cache';
import doQueue from '@opennextjs/cloudflare/overrides/queue/do-queue';


export default defineCloudflareConfig({
  incrementalCache: r2IncrementalCache,
  queue: doQueue,
  tagCache: d1NextTagCache,
});
Enter fullscreen mode Exit fullscreen mode

NBD. The hardest part here was changing the double quotes to single quotes.

By the way, can you believe that wrangler uses tabs in its generated types file? Unconscionable! How do these people make it out of bed in the morning!

.github/workflows/deploy.yml

name: Deploy Workers

on:
  push:
    branches:
      - main

jobs:
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    environment: production

    env:
      CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
      CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}

    steps:
      - uses: actions/checkout@v4

      # Install node and npm with cached pnpm installation.
      # https://github.com/pnpm/action-setup?tab=readme-ov-file#use-cache-to-reduce-installation-time
      - uses: pnpm/action-setup@v4
        with:
          run_install: false
      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'
          cache: 'pnpm'
      - run: pnpm install

      - name: Cache Next.js build
        uses: actions/cache@v4
        with:
          path: ${{ github.workspace }}/web/.next/cache
          # Generate a new cache whenever packages or source files change
          key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
          # If source files changed but packages didn't, rebuild from a prior cache
          restore-keys: |
            ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-

      - name: Deploy
        run: |
          cd web
          pnpm run deploy
Enter fullscreen mode Exit fullscreen mode

You need to add these repository secrets:

  • CLOUDFLARE_API_TOKEN
  • CLOUDFLARE_ACCOUNT_ID

About the API token. It needs (some if not all of) these permissions:

  • Your Account
    • Workers KV Storage:Edit
    • Workers Scripts:Edit
    • Account Settings:Read
    • Workers Tail:Read
    • Workers R2 Storage:Edit
    • Cloudflare Pages:Edit
    • Workers Builds Configuration:Edit
    • Workers Observability:Edit
    • D1:Edit
  • Your zone - Workers Routes:Edit
  • All users - User Details:Read
    • Memberships:Read

I got these permissions by choosing the API token preset called "Edit Cloudflare Workers". However, I also needed to add the D1:Edit permission.

If there is no D1:Edit permission, you'll see this error:

Creating D1 table if necessary...
ERROR Wrangler command failed
ELIFECYCLE  Command failed with exit code 1.
Enter fullscreen mode Exit fullscreen mode

So helpful, right? (I wonder if prefixing WRANGLER_LOG="debug" would make it more verbose.)

Technically the CLOUDFLARE_ACCOUNT_ID doesn't need to be a secret, but I'm doing what I know works.

This job has some npm caching stuff, and the actions are all up to date and using best practices.

Oh, remember to create a .nvmrc file containing the string 22, or whatever version of node you use.

Beware the line 33 has the path web. Change that if you don't use a monorepo or if your path is different.

Also the cd web line in the deploy step.

And here's a snippet from my package.json scripts:

    "opennext:build": "opennextjs-cloudflare build",
    "deploy": "pnpm run opennext:build && opennextjs-cloudflare deploy",
Enter fullscreen mode Exit fullscreen mode

That's all

Yeah I guess that's it!

Top comments (1)

Collapse
 
tythos profile image
Brian Kirkpatrick

Interesting. This makes me grateful for my usual Terraform-based approach to CF interactions. Doing infrastructure work with TS and wrangler does not look like something I'd be jealous of! Glad you figured it out.