How to Deploy a Next.js Project on Cloudflare
Admin
Author
Why Cloudflare instead of Vercel
Vercel is the obvious default for Next.js — it is built by the same team. But if you are already using Cloudflare for DNS, or you want a generous free tier for a side project, Workers is a real alternative.
The catch: Next.js was not built for Cloudflare’s runtime natively, so you need a translation layer called OpenNext to turn a standard Next.js build into something Workers can run.
This guide covers a real App Router setup on Cloudflare Workers — including two mistakes that each cost a full debugging session.
Installing OpenNext for Cloudflare
Two packages get you started:
npm install --save-dev @opennextjs/cloudflare wrangler --legacy-peer-deps
wrangler— Cloudflare’s CLI for deploying Workers@opennextjs/cloudflare— adapter that converts your.nextbuild into a Worker
open-next.config.ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
export default defineCloudflareConfig();
wrangler.jsonc
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "your-app-name",
"main": ".open-next/worker.js",
"compatibility_date": "2026-06-01",
"compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"observability": {
"enabled": true
}
}
The nodejs_compat flag matters more than it looks — without it, any dependency using Node built-ins like crypto or buffer fails at runtime, not build time.
Build scripts
{
"scripts": {
"dev": "next dev",
"build": "next build",
"cf:build": "opennextjs-cloudflare build",
"cf:preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
"cf:deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy"
}
}
cf:preview is the step people skip — and it is exactly what would have caught the middleware bug below before production.
Gotcha #1: middleware.ts vs proxy.ts
On Next.js 16, the framework nudges you toward proxy.ts instead of middleware.ts for request interception.
Problem: proxy.ts runs on the Node.js runtime. Cloudflare Workers via OpenNext only supports the Edge runtime for middleware. Rename to proxy.ts, deploy, and production silently stops running your middleware — no error, no warning. Auth redirects look like a Clerk/cookie bug.
Fix: keep the file named middleware.ts.
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
import { NextResponse } from "next/server";
const isDashboard = createRouteMatcher(["/dashboard(.*)"]);
const isPublicOnly = createRouteMatcher(["/", "/sign-in(.*)", "/sign-up(.*)"]);
export default clerkMiddleware(async (auth, req) => {
const { userId } = await auth();
if (userId && isPublicOnly(req)) {
return NextResponse.redirect(new URL("/dashboard", req.url));
}
if (!userId && isDashboard(req)) {
await auth.protect();
}
});
// middleware.ts (not proxy.ts): Cloudflare Workers via OpenNext only supports
// the Edge middleware runtime, and Next 16's proxy.ts is Node-only.
export const config = {
matcher: [
"/((?!_next|[^?]*\\\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
"/(api|trpc)(.*)",
],
};
If you deploy Next.js 16 to Cloudflare: ignore the proxy.ts migration guide for Workers and stay on middleware.ts.
Test with the real runtime before deploying
npm run cf:build
npm run cf:preview
next dev runs on Node no matter what your production target is. cf:preview runs the built Worker through workerd — Cloudflare’s real runtime — on your machine.
Environment variables: two buckets
- Variables — plain text. Use for
NEXT_PUBLIC_*values (baked into the client bundle at build time). - Secrets — encrypted. Use for Clerk secret keys, deploy keys, payment keys.
npx wrangler secret put CLERK_SECRET_KEY
NEXT_PUBLIC_* values must exist at build time, not only runtime. With Cloudflare Git integration, set them in that build’s environment screen — it is separate from Worker runtime variables.
Wiring up GitHub auto-deploy
- Cloudflare dashboard → Workers & Pages → Connect to Git
- Authorize GitHub and pick the repo
- Build command:
npm run cf:build - Add every
NEXT_PUBLIC_*value and secret in the build environment
After that, every push to your production branch builds and deploys automatically.
Gotcha #2: peer-dependency error only on Cloudflare
A GitHub-connected build can fail at install with a peer range error for @opennextjs/cloudflare, even when the same Next version installs fine locally. Cloudflare’s npm resolves multi-clause || peer ranges more strictly.
Add a root .npmrc:
legacy-peer-deps=true
Commit it, push, and the next auto-build gets past install cleanly.
Pitfalls worth remembering
- middleware.ts, not proxy.ts — Next.js 16 rename is Node-only and silently no-ops on Workers
- nodejs_compat — miss it and Node-dependent packages break at runtime
- NEXT_PUBLIC_* must be in the build environment or the client bundle gets
undefined - .npmrc with legacy-peer-deps=true — avoids Cloudflare install peer conflicts
- Always run cf:preview before deploying — only local step that uses Cloudflare’s runtime
None of this is documented clearly in one place, which is why these bugs are easy to ship. Get the middleware file name and .npmrc right from the start.

Comments
0Login to post a comment.
Sign inLoading comments…