$ cat ./blog/nextjs-cloudflare-deploy-pitfalls.md
Next.js を Cloudflare で動かすときのつまずき 10 選 — 2026 年の現行パス
Cloudflare 上で Next.js を動かす公式推奨パスは 2024 年後半に大きく変わりました。現行は @opennextjs/cloudflare + Cloudflare Workers、@cloudflare/next-on-pages + Pages は legacy 扱いです。本記事では Next.js 16 系を Cloudflare Workers に載せるときにハマるポイントを、原因と回避策つきで 10 項目まとめます。
- author
- r43
- date
- (2026年4月22日)
- reading
- 4 min
- commit
- 3351460
「Next.js を Cloudflare で動かす」は、2024 年後半に公式推奨パスが Pages から Workers へ移りました。2026-04 時点の正解は、Next.js 16 + @opennextjs/cloudflare adapter + Cloudflare Workers で、旧来の @cloudflare/next-on-pages + Pages は legacy 扱いです。本記事では現行パスを前提に、実際に詰まる 10 箇所を整理します。
到達点と前提
- Next.js 16(16.2 系)/ App Router 前提
@opennextjs/cloudflare v1.19 系
- Cloudflare Workers にデプロイ(Pages ではなく)
nodejs_compat フラグ有効、compatibility_date: "2024-09-23" 以降
1. デプロイ先の選び間違い(Workers か Pages か)
新規 Next.js プロジェクトは Workers + @opennextjs/cloudflare が推奨です。Pages で動かす @cloudflare/next-on-pages は引き続き動きますが、新機能(Server Actions 最適化 / Node API サポートの拡充 / assets binding 系)は Workers 側に集まっています。Pages → Workers 移行は削除せず新 Workers アプリで並行公開 → 切替が事故が少ないです。
2. 古い @cloudflare/next-on-pages を選んでしまう
社内テンプレや古いブログ記事をコピーすると、@cloudflare/next-on-pages が入ります。@cloudflare/next-on-pages は 2025-09 に GitHub リポジトリがアーカイブされ、公式に deprecated 済みです。package.json の devDep に @cloudflare/next-on-pages があり、npx @cloudflare/next-on-pages でビルドしているなら、@opennextjs/cloudflare への移行を先に検討してください。
3. nodejs_compat フラグ忘れ
@opennextjs/cloudflare は Node 互換 API に依存します。wrangler.jsonc に nodejs_compat を入れ忘れると、crypto / buffer / util 等を触るサーバーコードが実行時に ReferenceError で落ちます。
{
"main": ".open-next/worker.js",
"compatibility_date": "2024-09-23",
"compatibility_flags": ["nodejs_compat"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
}
}
4. compatibility_date が古い
nodejs_compat フラグは任意の日付でも指定できますが、2024-09-23 以降で nodejs_compat_v2 が自動有効化されます。@opennextjs/cloudflare は v2 前提で書かれているため、結果的に compatibility_date: "2024-09-23" 以降が実質必須です。新規プロジェクトなら執筆時点の最新(例: 2026-04-01)に合わせておくのが安全です。
5. next/image が本番で動かない
ローカル(next dev)では動くのに、Workers にデプロイすると next/image が 404 や 500 を返す — よくある症状です。Cloudflare Workers は Vercel の Image Optimization を内蔵しないため、Cloudflare Images または外部の loader を明示的に設定する必要があります。
next.config.ts で images.loader と images.loaderFile を設定するか、画像を <img> で直接提供する割り切りもよく使われます。小規模プロジェクトなら後者で十分です。
6. Middleware → Proxy のリネームと Node ランタイム化に追従していない
Next.js 16 で middleware.ts は proxy.ts にリネームされ、Node.js runtime が既定になりました(15.x 系の middleware と Edge runtime 前提はサポートされるが非推奨)。古い記事・テンプレートをベースに組むと、ファイル名や runtime 指定でハマります。
- Vercel の Edge Function の「1 MiB」制限を Next.js 本体の仕様と勘違いしない(これは Vercel プラットフォーム固有の値)
- Cloudflare Workers のスクリプト上限は Free 3 MiB / Paid 10 MiB(いずれも圧縮後)
proxy.ts に重いライブラリ(bcrypt、全機能 jose、巨大 i18n 辞書)を抱え込ませないのは引き続き鉄則。必要なら Route Handler へ逃がす
7. 古い「TCP が張れない」認識で Postgres / Prisma を組む
Cloudflare Workers は 2023 年に cloudflare:sockets の connect() API を公開しており、生の TCP ソケットを張れます。Postgres は pg + Prisma driver adapter 経由で、Cloudflare 上から直接接続可能です。
ハマるのは、Node の net モジュールに直接依存した古いライブラリや、旧来の Prisma 既定(driver adapter 未使用)のまま移植しようとするケース。現実的な選択肢は次のとおり。
- Cloudflare D1 (SQLite) — もっとも軽量、 Workers Paid の枠内で動く
- Hyperdrive 経由の Postgres — 既存 Postgres を低レイテンシで繋げる公式経路
- Neon / Supabase の HTTP driver(例:
@neondatabase/serverless)— TCP を使わない serverless 向け設計で、Edge/Node どちらでも素直
pg + Prisma driver adapter — Postgres 直接接続、driver adapter を使う
プロジェクト初期に「どれを使うか」を決めて、ローカルと本番で同じアダプタを踏むようにするのが事故を減らすコツです。
8. 環境変数の優先順位を間違える
Next.js 自身の .env.local / .env.production と、Wrangler の vars / secrets / Workers Dashboard の Environment Variables — 3 つの系統があります。
- ビルド時に埋め込みたい値(
NEXT_PUBLIC_*): Next.js 側の .env* に置く
- サーバー実行時に読む値 / 秘密情報:
wrangler secret put で Workers Secrets に登録
- Dashboard から編集したい値: Workers の Environment Variables(非機密用途)
同じキーを複数箇所に置くと、どれが効いているか追跡できなくなるので、運用ルールを最初に決めるのが一番コストが低いです。
9. ISR / Revalidation が Vercel と違う
Next.js の ISR は Vercel 上での動作が基準で、Cloudflare Workers では Next のキャッシュ機構と Workers のキャッシュ(Cache API / KV)の関係を自分で設計する必要があります。@opennextjs/cloudflare は incremental cache の backend を差し替えられる設計ですが、デフォルトで Vercel と同じ挙動になるわけではない点は最初から織り込んでおいてください。
10. 開発時(next dev)と本番(workerd)の挙動差で気づくのが遅れる
ローカルの next dev は Node.js で動き、本番は workerd ランタイムです。環境差に起因するバグは wrangler dev を使って早い段階で検出するのが定石です。
# @opennextjs/cloudflare の開発フロー
pnpm build # next build
pnpm opennextjs-cloudflare build # OpenNext が .open-next/ を生成
wrangler dev # ← workerd ランタイムで実行 (本番と同じ挙動)
wrangler deploy # 本番反映
CI にも wrangler dev 起動 → スモークテスト を挟むと、本番でしか出ないバグがデプロイ前に捕まります。
まとめ
Cloudflare での Next.js 運用は 2024 年後半で “正解” が Workers に寄ったのが実情です。新規プロジェクトなら @opennextjs/cloudflare + Workers 一択、既存 @cloudflare/next-on-pages + Pages 環境は急ぐ必要はないものの中長期で移行候補、という整理が 2026-04 時点の定石です。
次回は TypeScript + Zod でエッジランタイム向け型安全 API を書く予定です。
参考
$ tree ./repo/
記事で登場したコードをひとつの最小リポジトリとしてまとめました。ファイルツリーから切り替えて、全体の構造をそのまま確認できます。
┌─nextjs-cloudflare-deploy-pitfalls──────────files08──────────────────────●●●─┐$cat./wrangler.jsoncjsonc
# Next.js 側の環境変数。
# - NEXT_PUBLIC_* … クライアントバンドルに埋め込まれる
# - それ以外 … `next build` 時にサーバーバンドルへ埋め込まれる
#
# サーバー「実行時」に読みたい秘密情報 (API キー等) はここではなく
# `wrangler secret put KEY` で Workers Secrets に登録すること。
# 例: 公開しても安全なサイト URL
NEXT_PUBLIC_SITE_URL=http://localhost:3000
# 例: 公開しても安全な API エンドポイント
NEXT_PUBLIC_API_BASE=/api
# nextjs-cloudflare-starter
Next.js 16 を **Cloudflare Workers** で動かす最小テンプレート。
[記事本文](https://r43lab.com/blog/nextjs-cloudflare-deploy-pitfalls) と対になる実動構成です。
## Quick start
```bash
bun install
# 開発 (Next.js の Node サーバー)
bun run dev
# 本番同等で動かす (Workers ランタイム = workerd)
bun run preview # next build → opennextjs-cloudflare build → wrangler dev
# デプロイ
bun run deploy # next build → opennextjs-cloudflare build → wrangler deploy
```
## なぜ Workers 推奨?
2024 年後半から Cloudflare は Next.js の公式推奨パスを **`@opennextjs/cloudflare` + Cloudflare Workers** に切り替えました。旧来の `@cloudflare/next-on-pages` + Pages は引き続き動きますが、Node 互換や新機能の優先投入先は Workers 側です。
新規採用は Workers、既存 Pages 構成は中長期で移行、という整理が 2026-04 時点の定石。
## 要件
- Next.js 16 系 (App Router)
- `@opennextjs/cloudflare` v1.19 系
- `wrangler.jsonc` で **`nodejs_compat` フラグ** を有効化
- `compatibility_date` を **2024-09-23 以降**(このテンプレは `2026-04-01`)
## ハマりポイント
記事本文で 10 件解説していますが、特にデプロイ直後に気づきがちなのは:
- `next/image` は Cloudflare Images または loaderFile 指定が必須
- `pg` / Prisma の既定アダプタは動かない — D1 / Hyperdrive / Neon HTTP driver へ
- Middleware のバンドルを軽く保つ(重いライブラリを import しない)
## 依存バージョン
`package.json` は **2026-04 時点のスナップショット** です。Next.js と adapter は動きが速いので、clone 後に最新化を推奨。
```bash
bun outdated
bun update
```
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Next.js 本体の設定。Cloudflare 固有の設定は open-next.config.ts 側に寄せる。
images: {
// Cloudflare Workers には Vercel の Image Optimization がないため、
// どう配信するかを明示する必要がある。選択肢:
// (a) Cloudflare Images を使う (loaderFile で carrier を差し込む)
// (b) <img> で直接配信して next/image の最適化を使わない
// (c) 外部 CDN (imgix / Cloudinary 等) を loaderFile で繋ぐ
//
// 小規模プロジェクトなら unoptimized: true で十分なケースも多い。
unoptimized: true,
},
// Cloudflare のビルドは open-next adapter が面倒を見るので standalone 指定は不要。
};
export default nextConfig;
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
// OpenNext の Cloudflare adapter 設定。
// デフォルトの `defineCloudflareConfig()` だけで通常は十分動く。
// Incremental cache / queue / tag cache の backend を差し替えたい場合のみ指定する。
export default defineCloudflareConfig({
// incrementalCache: ... // (KV / D1 backend を差し替える場合)
// tagCache: ...
// queue: ...
});
{
"name": "nextjs-cloudflare-starter",
"private": true,
"version": "0.0.1",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"preview": "opennextjs-cloudflare build && wrangler dev",
"deploy": "opennextjs-cloudflare build && wrangler deploy",
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
},
"dependencies": {
"next": "^16.2.4",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@opennextjs/cloudflare": "^1.19.3",
"@types/node": "^22",
"@types/react": "^19",
"@types/react-dom": "^19",
"typescript": "^6.0.3",
"wrangler": "^4.84.0"
}
}
// Route Handler のサンプル。
// OpenNext 経由で Cloudflare Workers 上では Node 互換ランタイム
// (workerd + nodejs_compat) で実行される。
//
// export const runtime = "edge"; // ← 指定しても良いが、OpenNext + Workers では
// default (Node-like) のままで問題ないケースが多い
export async function GET(request: Request) {
const url = new URL(request.url);
const name = url.searchParams.get("name") ?? "world";
return Response.json({
greet: `hello, ${name}`,
runtime: "cloudflare-workers (workerd)",
at: new Date().toISOString(),
});
}
// App Router の最小ページ (Server Component)
export default function Home() {
return (
<main
style={{
fontFamily: "ui-monospace, monospace",
padding: "2rem",
lineHeight: 1.8,
}}
>
<h1>nextjs-cloudflare-starter</h1>
<p>Next.js 16 が Cloudflare Workers (workerd) で動いています。</p>
<h2>動作確認</h2>
<ul>
<li>
<code>GET /api/hello</code> — Route Handler (Edge/Node どちらでも可)
</li>
</ul>
<h2>次にやること</h2>
<ol>
<li>
<code>wrangler dev</code> で workerd ランタイム上のローカル確認
</li>
<li>
<code>wrangler secret put DATABASE_URL</code> でサーバー秘密情報を登録
</li>
<li>データアクセス層を D1 / Hyperdrive / Neon HTTP へ差し替え</li>
</ol>
</main>
);
}
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "nextjs-cloudflare-starter",
"main": ".open-next/worker.js",
// nodejs_compat を有効にするため、compatibility_date は 2024-09-23 以降が必要。
"compatibility_date": "2026-04-01",
"compatibility_flags": ["nodejs_compat"],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS"
},
"observability": {
"enabled": true
}
// サーバーサイドで参照する秘密情報は wrangler secret put で。
// wrangler secret put DATABASE_URL
// wrangler secret put SOME_API_KEY
//
// NEXT_PUBLIC_* のビルド時埋め込み値は .env.local / .env.production に。
}