Vercel Proプランで60以上のNext.jsプロジェクトを運用していましたが、組織向けアプリをCloudflare Pagesに移行することで、Hobbyプラン(無料)へのダウングレードを実現しました。

背景

Vercel Proの課題

Vercel Hobbyプランは非商用・個人利用のみです。組織のアプリをホストしている場合、Proプラン($20/月〜)を維持する必要があります。

しかし、60以上あるプロジェクトのうち組織向けアプリは2つだけでした。この2つをCloudflareに移せば、残りは個人プロジェクトとしてHobbyプランで運用できます。

移行対象

  • 多言語対応(日本語/英語)の検索アプリ(Next.js + next-intl + Elasticsearch)
  • IIIF画像ビューアアプリ

いずれもカスタムドメインで運用中。

Next.js 16の壁 — proxy.tsが未対応

最初にそのままCloudflare Pagesへの移行を試みましたが、ビルドの最終段階でエラーが発生しました。

Node.js middleware is not currently supported.
Consider switching to Edge Middleware.

Next.js 16では従来の middleware.tsproxy.ts にリネームされ、Node.jsランタイム固定になりました。@opennextjs/cloudflare v1.18.0時点ではこの新しい proxy.ts に未対応です(opennextjs/opennextjs-cloudflare#962)。

解決策:Next.js 15にダウングレード

# package.json
"next": "^15.3.1",        # 16.x → 15.x
"next-intl": "^3.26.5",   # 4.x → 3.x(Next.js 15対応版)
"eslint-config-next": "^15.3.1",

同時に proxy.tsmiddleware.ts にリネームし、エクスポート名も proxymiddleware に変更しました。

Cloudflare Pagesへのデプロイ手順

1. 依存パッケージのインストール

npm install --save-dev @opennextjs/cloudflare wrangler

2. 設定ファイルの作成

// open-next.config.ts
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
export default defineCloudflareConfig({});
// wrangler.jsonc
{
  "name": "my-app",
  "pages_build_output_dir": ".open-next/assets",
  "compatibility_date": "2025-04-01",
  "compatibility_flags": ["nodejs_compat"]
}

3. ビルド

npx @opennextjs/cloudflare build

4. Pagesへのデプロイ(ここが最大のポイント)

@opennextjs/cloudflare のビルド出力はWorkers向けですが、Pagesにデプロイするには以下の手順が必要です。

# Pagesプロジェクト作成
npx wrangler pages project create my-app --production-branch main

# _worker.jsをコピー(PagesのWorkerエントリーポイント)
cp .open-next/worker.js .open-next/_worker.js

# assetsの中身を.open-nextルートにコピー(静的ファイル配信用)
cp -r .open-next/assets/* .open-next/

# デプロイ(.open-nextディレクトリ全体)
npx wrangler pages deploy .open-next --project-name my-app --branch main

3つのポイント:

  1. _worker.jsのコピー — Pagesは _worker.js をWorkerエントリーポイントとして認識する
  2. assetsの展開assets/内のファイル(_next/static/等)を.open-next/ルートにコピーしないと、CSSやJSが404になる
  3. .open-nextディレクトリ全体をデプロイassets/だけではserver-functions/等のWorker依存ファイルが含まれない

5. _routes.json で静的ファイルをWorkerから除外

これがないと、CSSやJavaScript等の静的ファイルまでWorker(Next.jsサーバー)に渡されて404になります。

// .open-next/_routes.json
{
  "version": 1,
  "include": ["/*"],
  "exclude": [
    "/_next/static/*",
    "/favicon.ico",
    "/apple-icon.png",
    "/icon.png",
    "/manifest.json",
    "/sitemap.xml",
    "/robots.txt",
    "/images/*",
    "/*.webp",
    "/*.png",
    "/*.jpg",
    "/*.css",
    "/*.js"
  ]
}

excludeに指定したパスはPagesの静的ファイル配信で直接返され、それ以外は_worker.js(Next.jsサーバー)で処理されます。

6. 環境変数の設定

# サーバー側の機密情報はsecretsとして設定
npx wrangler pages secret put ES_URL --project-name my-app
npx wrangler pages secret put ES_USER --project-name my-app
npx wrangler pages secret put ES_PASSWORD --project-name my-app

NEXT_PUBLIC_* 変数はビルド時に埋め込まれるため、ビルド前に .env.local に設定しておきます。

git worktreeでの安全な実験

既存のVercelデプロイに影響を与えずにCloudflare移行を試すため、git worktreeを使いました。

# developブランチから実験用worktreeを作成
git worktree add ../my-app-cloudflare -b feature/cloudflare-migration develop

これにより、元のリポジトリはそのまま(Vercelでデプロイ中)で、別ディレクトリでCloudflare向けの変更を試せます。

Workers vs Pages — なぜPagesを選んだか

項目WorkersPages
カスタムドメインCloudflare DNSゾーン必須CNAMEだけでOK
GitHub連携手動設定ダッシュボードから簡単
プレビューデプロイ自分で設定PRごとに自動
リクエスト数(無料)10万/日無制限

最初はWorkersにデプロイして動作確認しましたが、カスタムドメインにCloudflare DNSゾーンが必要と判明。既存のDNS(さくら、Route 53等)をそのまま使いたかったため、Pagesに切り替えました。

Vercel(Hobby)で注意すべきこと

Hobbyプランへのダウングレード後の制限:

項目ProHobby
帯域幅1TB/月100GB/月
サーバーレス関数実行時間900秒60秒
SSR実行リージョン選択可能iad1(米国東部)のみ
チーム利用個人のみ
商用利用非商用のみ

特にSSRのリージョン制限は重要です。Hobbyプランでは日本からのSSRリクエストが太平洋を往復するため、レイテンシが増加します。一方、Cloudflare Pages(Workers)は東京を含む全世界のEdgeで実行されるため、SSRのレスポンスはCloudflareの方が高速です。

移行可否の判断基準

条件移行可否
Next.js 15以下 + API Routes のみ✅ 移行しやすい
Next.js 15以下 + Edge Middleware✅ 対応済み
Next.js 16 + proxy.ts❌ 未対応(15に下げれば可)
next-intl 等の国際化ライブラリ✅ middleware.ts経由で動作
Elasticsearch等のTCPライブラリ⚠️ fetchへの書き換えが必要な場合あり
sharp等のネイティブモジュール❌ Workers環境では動作しない

まとめ

  • Vercel Hobbyプランは非商用・個人利用限定。組織アプリがあるならProが必要
  • 組織アプリだけCloudflare Pagesに移せば、残りはHobbyで運用可能
  • Next.js 16の proxy.ts はopennextjs-cloudflareで未対応。15に下げれば移行できる
  • Pagesにデプロイする際は _worker.js のコピー、assetsの展開、_routes.json の配置が必要
  • カスタムドメインを既存DNSで管理したいなら Workers ではなく Pages を使う
  • git worktreeで安全に実験してから移行するのがおすすめ