CloudflareにNext.jsをデプロイする(後編)〜環境変数・カスタムドメイン・ISR/SSR実践設定

2026.02.27 09:00
2026.03.16 13:32
CloudflareにNext.jsをデプロイする(後編)〜環境変数・カスタムドメイン・ISR/SSR実践設定

前編では、OpenNextを使ってNext.jsをCloudflare Workersにデプロイする基本的な流れを紹介しました。
後編では、実際に運用するときに必要になる設定をまとめていきます。

環境変数、カスタムドメイン、SSR/ISR/Staticの使い分け、キャッシュ戦略あたりを扱います。

環境変数の設定

Next.jsで環境変数を使う場面は多いですが、Cloudflare Workersでは設定方法がいくつかあります。

ビルド時の環境変数(NEXT_PUBLIC_xxx)

NEXT_PUBLIC_ で始まる変数は、ビルド時にクライアント側のJSに埋め込まれます。
これはCloudflareのダッシュボード、またはGitHub Actionsのシークレットで設定します。

Cloudflareダッシュボードの場合:

  1. Workers & Pages → 対象のWorkerを選択
  2. Settings → Variables and Secrets
  3. 「Add」で変数を追加

GitHub Actionsの場合:

ワークフロー内でビルドコマンドに環境変数を渡します。

      - run: npx opennextjs-cloudflare build
        env:
          NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL }}
          NEXT_PUBLIC_SITE_NAME: ${{ vars.NEXT_PUBLIC_SITE_NAME }}

ランタイムの環境変数(サーバー側)

サーバー側(API Route、Server Componentsなど)で使う環境変数は、wrangler.jsonc[vars] セクション、またはCloudflareダッシュボードで設定します。

{
  "name": "my-next-app",
  "main": ".open-next/worker.js",
  "compatibility_date": "2025-04-01",
  "compatibility_flags": ["nodejs_compat"],
  "vars": {
    "DATABASE_URL": "postgresql://localhost:5432/mydb",
    "API_SECRET": "your-secret-here"
  },
  "assets": {
    "directory": ".open-next/assets",
    "binding": "ASSETS"
  }
}

重要:compatibility_date"2025-04-01" 以降にしておかないと、process.env に値が入りません。前編でも触れましたが、ここが原因で「環境変数が読めない!」となるケースが多いです。

シークレット(パスワードやAPIキーなど)は、wrangler.jsoncに直接書かずに、Cloudflareダッシュボードの「Variables and Secrets」で「Encrypt」にチェックを入れて設定するのがおすすめです。

カスタムドメインの設定

デプロイ直後は xxx.workers.dev というURLですが、独自ドメインを設定できます。

方法1:Cloudflareダッシュボードから設定

いちばん簡単な方法です。

  1. Workers & Pages → 対象のWorkerを選択
  2. Settings → Domains & Routes
  3. 「Add」→「Custom Domain」
  4. ドメイン名を入力(例:app.example.com

CloudflareにDNSを任せているドメインなら、DNSレコードもSSL証明書も自動で設定されます。
何も考えなくていいので楽です。

方法2:wrangler.jsonc で設定

コードで管理したい場合は、wrangler.jsonc に書くこともできます。

{
  "name": "my-next-app",
  "main": ".open-next/worker.js",
  "compatibility_date": "2025-04-01",
  "compatibility_flags": ["nodejs_compat"],
  "routes": [
    {
      "pattern": "app.example.com",
      "custom_domain": true
    }
  ],
  "assets": {
    "directory": ".open-next/assets",
    "binding": "ASSETS"
  }
}

custom_domain: true を指定すると、そのドメインのすべてのパスがWorkerにルーティングされます。
DNSレコードと証明書もCloudflareが自動で設定してくれます。

なお、カスタムドメインを使うには、そのドメインのDNSがCloudflareで管理されている必要があります。
他のDNSサービスを使っている場合は、先にCloudflareにネームサーバーを移行しておく必要がありますね。

SSR / ISR / Static の使い分け

Next.jsのレンダリング方式は、Cloudflare Workers上でもちゃんと動きます。
ただし、それぞれ挙動の特徴があるので整理しておきます。

Static(静的生成)

ビルド時にHTMLが生成されて、そのまま配信されるパターンです。
いちばんシンプルで速いです。

// 何も指定しなければデフォルトでStaticになる
export default function AboutPage() {
  return <h1>About</h1>;
}

Cloudflare Workersでは、静的ファイルはアセットとして配信されるので、Workerの実行回数にカウントされません。コスト面でもいちばん有利です。

SSR(サーバーサイドレンダリング)

リクエストのたびにサーバーでHTMLを生成するパターンです。
ユーザーごとに異なるコンテンツを返したいときに使います。

// dynamic を指定するとSSRになる
export const dynamic = "force-dynamic";

export default async function DashboardPage() {
  const data = await fetch("https://api.example.com/user", {
    cache: "no-store",
  });
  const user = await data.json();

  return <h1>Welcome, {user.name}</h1>;
}

Cloudflare Workers上でSSRすると、ユーザーに近いエッジで実行されるので、レスポンスは速いです。
ただし、毎回Workerが実行されるので、リクエスト数に応じてコストが発生します。

ISR(Incremental Static Regeneration)

静的生成 + バックグラウンド再生成の組み合わせです。
「基本はキャッシュを返すけど、一定時間ごとに裏で再生成する」という動きをします。

// revalidate を指定するとISRになる
export const revalidate = 60; // 60秒ごとに再生成

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const res = await fetch(`https://api.example.com/posts/${params.slug}`);
  const post = await res.json();

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

ブログやニュースサイトのように「更新はあるけどリアルタイム性はそこまで要らない」コンテンツに向いています。

ただし、CloudflareでISRをちゃんと動かすには、キャッシュの設定が必要です。
次のセクションで詳しく説明します。

ISRのキャッシュ設定(R2を使う)

Vercelなら何も設定しなくてもISRが動きますが、CloudflareではキャッシュストレージとしてR2(オブジェクトストレージ)を自分で設定する必要があります。

R2バケットを作成

Cloudflareダッシュボード → R2 Object Storage → 「Create bucket」でバケットを作ります。
名前は何でもいいですが、例として my-next-cache にします。

wrangler.jsonc にR2バインディングを追加

{
  "name": "my-next-app",
  "main": ".open-next/worker.js",
  "compatibility_date": "2025-04-01",
  "compatibility_flags": ["nodejs_compat"],
  "assets": {
    "directory": ".open-next/assets",
    "binding": "ASSETS"
  },
  "r2_buckets": [
    {
      "binding": "NEXT_CACHE_R2",
      "bucket_name": "my-next-cache"
    }
  ]
}

バインディング名は NEXT_CACHE_R2 にします。OpenNextがこの名前でR2を探します。

open-next.config.ts でR2キャッシュを有効にする

import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";

export default defineCloudflareConfig({
  incrementalCache: r2IncrementalCache,
});

これでISRのキャッシュがR2に保存されるようになります。

時間ベースの再検証(revalidate)を使う場合

revalidate = 60 のような時間ベースの再検証を使う場合は、キューの設定も必要です。
Durable Objectを使ったキューが、再検証リクエストの重複排除をやってくれます。

import { defineCloudflareConfig } from "@opennextjs/cloudflare";
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue";

export default defineCloudflareConfig({
  incrementalCache: r2IncrementalCache,
  queue: doQueue,
});

なお、オンデマンド再検証(revalidatePath()revalidateTag())だけを使う場合は、キューの設定は不要です。

オンデマンド再検証(revalidateTag / revalidatePath)

CMS連携などで「コンテンツが更新されたらキャッシュを消す」という使い方をしたい場合は、オンデマンド再検証が便利です。

import { revalidateTag } from "next/cache";
import { NextRequest } from "next/server";

export async function POST(request: NextRequest) {
  const { tag, secret } = await request.json();

  // シークレットで認証
  if (secret !== process.env.REVALIDATION_SECRET) {
    return Response.json({ message: "Invalid secret" }, { status: 401 });
  }

  revalidateTag(tag);
  return Response.json({ revalidated: true });
}

CMSのWebhookからこのAPIを叩けば、特定のタグに紐づくキャッシュだけをピンポイントで更新できます。

Cloudflareバインディングへのアクセス

Cloudflare WorkersにはR2やD1(SQLiteデータベース)、KVなどのバインディングがあります。
Next.jsのServer ComponentsやAPI Routeからこれらにアクセスするには、getCloudflareContext を使います。

import { getCloudflareContext } from "@opennextjs/cloudflare";

export async function GET() {
  const { env } = await getCloudflareContext();

  // KVにアクセス
  const value = await env.MY_KV.get("some-key");

  // R2にアクセス
  const object = await env.MY_BUCKET.get("some-file.txt");

  // D1にアクセス
  const { results } = await env.MY_DB.prepare("SELECT * FROM users").all();

  return Response.json({ value, results });
}

Vercelにはない、Cloudflareならではの強みですね。
R2やD1を組み合わせれば、外部DBなしでフルスタックアプリが作れます。

ハマりポイント(後編)

NPMパッケージのエクスポート解決

一部のNPMパッケージは複数のエクスポート先を定義していて、Wranglerがバンドルするときにどれを使うか迷うことがあります。
その場合、wrangler.jsonc でNodeのエクスポートを優先するように指定できます。

{
  "build": {
    "conditions": ["workerd", "worker", "browser", "node"]
  }
}

パッケージによってはこの設定で解決しないこともありますが、まず試す価値はあります。

FinalizationRegistryエラー

ReferenceError: FinalizationRegistry is not defined というエラーが出たら、compatibility_date"2025-05-05" 以降に更新すると解決します。
古い互換日だと、このAPIが利用できません。

画像最適化(next/image)の注意点

Next.jsの <Image> コンポーネントによる画像最適化は、Cloudflare Workers上でも動作します。
ただし、最適化処理にはCPU時間を消費するので、大量の画像がある場合はCloudflare Imagesとの併用も検討してみるといいかもしれません。

ローカル開発時の注意

next dev での開発時はNode.js環境で動くので、Cloudflare固有のバインディング(R2やKVなど)は使えません。
バインディングを使ったコードを開発したいときは、npm run preview(Wranglerのローカルサーバー)を使う必要があります。

本番運用のチェックリスト

最後に、本番に出す前にチェックしておきたいことをまとめます。

  • compatibility_date は最新に近い日付になっているか
  • nodejs_compat フラグは有効か
  • シークレット(APIキー等)はダッシュボードのEncryptで設定しているか
  • export const runtime = "edge" が残っていないか
  • ISRを使うならR2バケットとバインディングは設定済みか
  • カスタムドメインのDNSはCloudflareで管理されているか
  • Workerのサイズが制限内に収まっているか

まとめ

前編・後編をとおして、CloudflareにNext.jsをデプロイする流れを一通り紹介しました。

  • 環境変数は compatibility_date を新しくすれば process.env で普通に使える
  • カスタムドメインはCloudflare DNSを使っていればほぼワンクリック
  • ISRにはR2のキャッシュ設定が必要(Vercelほど「自動」ではない)
  • D1やR2、KVなどCloudflare独自のサービスと組み合わせると、かなり強力

Vercelと比べると、設定する部分は少し多いです。
でもその分、帯域無制限・エッジでのSSR・Cloudflareエコシステムとの統合など、メリットも大きいです。

特にCloudflareをすでにCDNやDNSで使っている人には、かなりおすすめできる構成だと思いました。

今回は以上です!