Deploying a Next.js Site to Cloudflare Workers - The Full Walkthrough
This site runs on Cloudflare Workers. Not Pages. Not Vercel. Workers. It took a few attempts to get right. Here's the full process for anyone doing the same thing.
Why Workers Instead of Pages
Cloudflare has two hosting products - Pages and Workers. Pages is simpler but limited. Workers give you:
- Full server-side rendering capability (not just static)
- Access to Cloudflare KV, Durable Objects, R2 storage
- Custom routing and middleware at the edge
- The same global network (300+ data centers)
- No cold starts on the free tier
For a static blog you could use Pages. But I wanted the flexibility to add API routes, server-side features, and edge logic later without migrating platforms.
Prerequisites
- A Cloudflare account (free tier works)
- A domain pointed to Cloudflare DNS
- A GitHub repo with your Next.js project
- Node.js 18+
Step 1: Project Setup
Start with a standard Next.js project:
npx create-next-app@latest my-site --typescript --tailwind --app
cd my-site
Make sure your next.config.ts does NOT have output: "export". Workers needs the full Next.js build, not a static export:
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
unoptimized: true,
},
};
export default nextConfig;
images.unoptimized is needed because Cloudflare Workers don't support Next.js Image Optimization out of the box.
Step 2: Install OpenNext Adapter
Cloudflare Workers can't run Next.js natively. The OpenNext adapter converts a Next.js build into a Worker-compatible bundle.
npm install @opennextjs/cloudflare wrangler --save-dev
Create open-next.config.ts in the project root:
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
export default defineCloudflareConfig({});
Create wrangler.jsonc:
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-site",
"compatibility_flags": ["nodejs_compat"],
"compatibility_date": "2025-09-27",
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"main": ".open-next/worker.js"
}
Add build scripts to package.json:
{
"scripts": {
"build:cloudflare": "npx @opennextjs/cloudflare build",
"preview:cloudflare": "npx wrangler dev",
"deploy": "npm run build:cloudflare && npx wrangler pages deploy .open-next/assets --project-name=my-site"
}
}
Step 3: Test Locally
npm run build:cloudflare
npm run preview:cloudflare
Open http://localhost:8787. If you see your site, the adapter is working.
Step 4: Connect to Cloudflare
In the Cloudflare dashboard:
- Go to Workers & Pages
- Create a new Worker
- Connect your GitHub repository
- Set build settings:
- Build command:
npx @opennextjs/cloudflare build - Build output directory: leave empty (wrangler handles it)
- Build command:
Cloudflare will clone your repo, run the build, and deploy the Worker.
Step 5: Custom Domain
- In your Worker settings, go to Triggers (or Custom Domains)
- Add your domain (e.g.,
oljasz.com) - Cloudflare automatically handles SSL and DNS routing
If your domain is already on Cloudflare DNS, the whole process takes about 30 seconds.
What Went Wrong (Lessons Learned)
Filesystem Access Doesn't Work at Runtime
My blog reads Markdown files from a content/blog/ directory using fs.readFileSync. This works fine during next build because the build runs on a real machine with a real filesystem. But at runtime on Workers, there is no filesystem.
The fix: make sure all pages that use fs are pre-rendered at build time. Add export const dynamic = "force-static" to any page that reads files:
export const dynamic = "force-static";
This tells Next.js to render the page during build, not at request time. The HTML gets baked into the Worker bundle.
Blog Listing Was Empty
Even with force-static, my blog listing page was empty in production. The page used fs.readdirSync to scan the content directory. During the OpenNext build, the working directory is different from where the content files live.
The fix: hardcode the post metadata in the listing page instead of scanning the filesystem. Individual post pages work fine because they're pre-rendered via generateStaticParams which runs during next build.
// Don't do this on Workers:
const posts = fs.readdirSync("content/blog");
// Do this instead:
const posts = [
{ slug: "my-post", title: "My Post", date: "2026-03-15", ... },
];
Static Export Breaks OpenNext
If you have output: "export" in next.config.ts, the Next.js build generates static HTML files in an out/ directory. But OpenNext expects a full .next/ build with server functions. The build fails with:
ENOENT: no such file or directory, open '.next/standalone/.next/server/pages-manifest.json'
Remove output: "export" and let OpenNext handle the conversion.
Wrangler Auto-Config Runs Twice
When you run npx wrangler deploy, Wrangler detects Next.js and tries to auto-configure OpenNext. This adds packages, modifies package.json, creates config files, and runs the OpenNext build itself. If you already have these set up manually, it creates conflicts.
To avoid this, commit your wrangler.jsonc, open-next.config.ts, and build scripts before deploying. Wrangler won't override existing configs.
deploy.resources.reservations vs runtime: nvidia
Not a Cloudflare issue, but worth mentioning since it bit me on the GPU server side. Docker Compose's deploy.resources.reservations.devices syntax for GPU access only works with Docker Swarm. With regular docker compose, use runtime: nvidia instead. Unrelated to Workers but cost me hours of debugging when Ollama was running at CPU speed inside a container.
The Build Pipeline
Final flow:
git push
-> Cloudflare detects push
-> Clones repo
-> Runs: npx @opennextjs/cloudflare build
-> Runs: next build (generates .next/)
-> OpenNext converts .next/ to .open-next/
-> Bundles worker.js + static assets
-> Deploys to 300+ edge locations
-> Custom domain routes through Cloudflare DNS
-> SSL automatic
Total deploy time: about 60 seconds from push to live.
Cost
Free tier covers:
- 100,000 requests/day
- 10ms CPU time per request (plenty for static pages)
- Custom domains
- SSL
- DDoS protection
For a personal site or blog, you'll never hit these limits. I haven't paid anything for hosting this site.
Files to Add to .gitignore
.next
.open-next
.wrangler
.dev.vars
The .open-next directory is generated by the build and doesn't need to be in version control. Cloudflare builds it fresh on every deploy.
Summary
Workers with OpenNext is the right choice if you want server-side capability with Cloudflare's edge network. For a pure static site, Pages would be simpler. But the flexibility of Workers means you won't need to migrate when you add your first API route or server-side feature.
The main thing to watch out for: no filesystem at runtime. Pre-render everything you can during build, hardcode what you can't, and test locally with wrangler dev before pushing.