Menu

Deploy Next.js Project with CloudFlare Workers

Vercel offers the best developer experience as a deployment platform, but costs can easily skyrocket once your site gains traffic. Dokploy and Coolify are excellent VPS hosting tools that allow unlimited deployments with just one VPS purchase, but you need to ensure VPS security yourself.

CloudFlare Workers provides a service for only $5 per month in base subscription fees, with benefits including 10 million free requests per month and 30 million milliseconds of CPU execution time. Compared to Vercel, it offers significantly better value, which is why more and more people are trying CloudFlare Workers deployment.

However, CloudFlare Workers isn't all advantages - their deployment experience is quite unfriendly. You need to read extensive documentation and community knowledge to successfully deploy.

The NEXTY.DEV boilerplate has created a separate cf-pg branch specifically for CloudFlare Workers deployment requirements, tailored for CloudFlare Workers + Postgres deployment.

This branch supports using Neon, Supabase, and self-hosted Postgres on CloudFlare Workers.

This article is a deployment tutorial for the NEXTY.DEV boilerplate cf-pg branch on CloudFlare Workers.

Step 1: Configure CLI Environment

Before starting configuration, ensure you have installed wrangler (Cloudflare's command-line tool) and completed account login.

Execute the following command in the terminal:

npx wrangler login

This will automatically open your browser to complete authorization. This is a prerequisite for all subsequent R2, D1, and deployment operations.

Step 2: Create wrangler.jsonc

This is the wrangler.example.jsonc that comes with the cf-pg branch of the boilerplate:

{
	"$schema": "node_modules/wrangler/config-schema.json",
	"name": "<YOUR_WORKER_APP_NAME>",
	"main": ".open-next/worker.js",
	// When you start your project, you should always set compatibility_date to the current date
	// https://developers.cloudflare.com/workers/configuration/compatibility-dates/
	"compatibility_date": "2026-02-22",
	"compatibility_flags": [
		"nodejs_compat",
		"global_fetch_strictly_public"
	],
	// Your Cloudflare Account ID: Cloudflare Dashboard → Workers & Pages → Overview → Account ID
	"account_id": "<YOUR_ACCOUNT_ID>",
	"assets": {
		"directory": ".open-next/assets",
		"binding": "ASSETS"
	},
	"services": [
		{
			// Required by @opennextjs/cloudflare for ISR/revalidation
			"binding": "WORKER_SELF_REFERENCE",
			"service": "<YOUR_WORKER_APP_NAME>"
		}
	],
	"images": {
		"binding": "IMAGES"
	},
	"observability": {
		"logs": {
			"enabled": true,
			"invocation_logs": true
		}
	},
	"vars": {
		"DEPLOYMENT_PLATFORM": "cloudflare"
	},
	// --- Incremental Cache (R2) ---
	// Required for ISR/SSG caching
	"r2_buckets": [
		{
			"binding": "NEXT_INC_CACHE_R2_BUCKET",
			"bucket_name": "<YOUR_BUCKET_NAME>"
		}
	],
	// --- Queue (Durable Objects) ---
	// Required for time-based revalidation (ISR)
	"durable_objects": {
		"bindings": [
			{
				"name": "NEXT_CACHE_DO_QUEUE",
				"class_name": "DOQueueHandler"
			}
		]
	},
	"migrations": [
		{
			"tag": "v1",
			"new_sqlite_classes": [
				"DOQueueHandler"
			]
		}
	],
	// --- Tag Cache (D1) ---
	// Required for On-demand revalidation (revalidatePath/revalidateTag)
	// Create D1 database: npx wrangler d1 create <YOUR_D1_DATABASE_NAME>
	// Then update the database_id below
	"d1_databases": [
		{
			"binding": "NEXT_TAG_CACHE_D1",
			"database_name": "<YOUR_D1_DATABASE_NAME>",
			"database_id": "<YOUR_D1_DATABASE_ID>"
		}
	],
  // Use Hyperdrive for TCP-based PostgreSQL connections (Supabase, Self-hosted, or Neon via TCP).
  // You can omit this if using provider-specific HTTP drivers (e.g., @neondatabase/serverless).
	"hyperdrive": [
		{
			"binding": "HYPERDRIVE",
			"id": "<YOUR_HYPERDRIVE_ID>",
			"localConnectionString": "<YOUR_POOLER_CONNECTION_STRING>"
		}
	]
}

You need to copy this file and name it wrangler.jsonc, then customize the following parameters:

  1. name: Customize this

  2. compatibility_date: Fill in the project creation date

  3. account_id: Copy from Cloudflare Dashboard → Workers & Pages → Overview → Account ID

  4. WORKER_SELF_REFERENCE service: Fill in the same value as the name field

  5. NEXT_INC_CACHE_R2_BUCKET bucket_name:

Execute npx wrangler r2 bucket create <YOUR_BUCKET_NAME> in the command line, replacing <YOUR_BUCKET_NAME> with a custom name. After creation, a bucket_name will be generated. Copy it to the file. Do not change the binding.

  1. d1_databases:

Execute npx wrangler d1 create <YOUR_D1_DATABASE_NAME> in the command line, replacing <YOUR_D1_DATABASE_NAME> with a custom name. After creation, database_name and database_id will be generated. Copy them to the file. Do not change the binding.

  1. hyperdrive:

If you're using Neon, comment out the hyperdrive configuration, because Neon has already set up a proxy on the server side and is specifically optimized for Serverless environments (using @neondatabase/serverless), which can bypass TCP connection limitations.

If using Supabase or self-hosted Postgres, they are standard Postgres services that are limited by connection counts, and TCP connection latency will be relatively high, so you need to enable Hyperdrive. Please follow these steps:

a. Create Hyperdrive: Create an instance in CF dashboard under Workers & Pages → Hyperdrive.

create-hyperdrive
create-hyperdrive

b. Supabase Configuration: Copy the direct connection string for port 5432. This is because Hyperdrive itself is a connection pool, and Supabase's port 6543 is also a connection pool. If you mount a connection pool on a connection pool, it becomes very unstable.

connect-string

Paste the direct connection string into Hyperdrive and uncheck Enable caching

create-hyperdrive

c. Bind ID: Fill the generated Hyperdrive ID into wrangler.toml.

create-hyperdrive

d. Local Development: Fill localConnectionString with the port 6543 connection string, and the DATABASE_URL environment variable should also use the port 6543 connection string.

Step 3: Automated Deployment and Configuration

Whether using Neon or Supabase, the steps are the same.

Create project, Deploy

create-application

Project Name must match the name field in wrangler.jsonc

create-application

The first build will definitely fail - don't worry.

Go to Settings, scroll to the bottom first, and modify Github settings:

  • Add all production environment variables. This step can only be done manually. Environment variables starting with NEXT_PUBLIC_ can be added directly, while other environment variables need to be encrypted by clicking the Encrypt button. When adding environment variables, don't copy the quotes.
  • Set Build cache to Disabled
create-application

If your project has multiple branches and you want only specific branches to trigger builds, you can modify Branch control, uncheck Builds for non-production branches, and then push the branch code.

create-application

Then scroll to the top of the page and add a domain in Domains & Routes

create-application

There are also runtime environment variables here. You can only add them manually one by one on the page, but the boilerplate has a built-in script. You just need to execute node scripts/sync-env-to-cloudflare.mjs .env to submit the environment variables in .env to Workers. You can specify the environment variable file you want to migrate, i.e., .env can be changed to .env.local.

Since NEXT_PUBLIC_ environment variables have already been added in Build environment variables, there's no need to add NEXT_PUBLIC_ environment variables in runtime environment variables.

After updating environment variables, rebuild. You can either push code to trigger a build, or find the latest build record in Developments and click Retry Build.

Now you can access your custom domain.