AI個人開発の技術スタック

Next.jsをCloudflare Workersで動かす

サーバーを自分で持たずにNext.jsアプリを公開する方法として、Cloudflareが2026年に「vinext」という新しい仕組みを公開しました。無料枠は商用利用も可能なほど太い一方、実験的な段階であることも踏まえて選ぶ必要があります。

自分でサーバーを持たない、という選択

この手引の基本構成は、Linuxサーバーを自分で用意し、systemdとCaddyでNext.jsを動かす方法です(Next.jsの基礎、Caddyの基礎を参照)。Cloudflare Workersはその逆で、サーバーの管理をCloudflare側に任せ、世界中のエッジでコードを実行する仕組みです。無料プランは商用利用も許可されており、個人開発や小規模なサービスであれば無料枠だけで公開まで完結できる場合があります。

vinextはCloudflareが作った新しいNext.js互換レイヤー

2026年2月、CloudflareはVite上でNext.jsのAPIを再実装した「vinext」を公開しました。従来のアダプターであるOpenNextに代わり、新規プロジェクトの標準ルートとして案内されています。Next.js 16のAPIカバレッジは9割程度、ビルド速度やバンドルサイズでも優位とされていますが、公式に「実験的(experimental)」と位置づけられている点は理解した上で使います。

npx @vinext/cloudflare deploy

wrangler loginで認証した状態であれば、この1コマンドでビルドからCloudflare Workersへのデプロイまで完了します。D1やKVなどのバインディングは、サーバー側のコードからcloudflare:workers経由で読み込みます。

既存プロジェクトの移行は急がない:本番で動いているNext.jsアプリを今すぐvinextへ移行する必要はありません。実験的な機能である以上、まずは新規の小さいプロジェクトで試すのが安全です。

D1(データベース)とR2(ファイル保存)を使い分ける

Cloudflare D1はSQLiteをベースにしたデータベースで、Workersから直接呼び出せます。Sessions APIによるグローバルなリードレプリケーションにも対応し、世界複数リージョンへ読み取りを分散できます。ただし書き込みは1つのプライマリに集約される仕組みのままで、目安として秒間200件を超える書き込みが続くとロック待ち(SQLITE_BUSY)が発生し始めます。

画像やPDFなどのファイルはD1ではなくR2(オブジェクトストレージ)に保存します。S3互換のAPIを持ち、最大の特徴は転送量(エグレス)に課金されないことです。ユーザーがアップロードした画像を配信するようなNext.jsアプリでは、D1に構造化データ、R2にファイル本体という役割分担が基本になります。

向いているのは:読み取りが中心の個人開発・小規模サービスです。会員数が多く書き込みが集中する業務システムには不向きで、その場合はNext.jsの基礎で紹介しているPostgreSQL構成の方が安定します。

D1・R2のバインディングと、デプロイの設定例

D1とR2は、Workerのコードからenv.DBのように参照できるようにするため、wrangler.jsoncに「バインディング」として登録します。最小構成は次のようになります。

{
  "name": "my-app",
  "compatibility_date": "2026-09-01",
  "assets": { "directory": "./dist" },
  "d1_databases": [
    { "binding": "DB", "database_name": "my-app-db", "database_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }
  ],
  "r2_buckets": [
    { "binding": "UPLOADS", "bucket_name": "my-app-uploads" }
  ]
}

D1のデータベースとR2のバケットは、事前にコマンドで作成しておきます。

wrangler d1 create my-app-db
wrangler d1 migrations apply my-app-db --remote
wrangler r2 bucket create my-app-uploads

デプロイはローカルからwrangler deployを直接叩くのではなく、GitHubへのpushをきっかけにGitHub Actionsから実行する構成にします。手元の端末にデプロイ権限を持たせないための基本的な考え方は、この手引のデプロイの基礎と同じです。

name: Deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run build
      - uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          command: deploy
実際にこの構成で運用しています:このRadarWorksのサイト自体、GitHub Actionsからのwrangler deployだけをデプロイ経路にしており、開発者の手元からの直接デプロイは行っていません。CloudflareのAPIトークンはGitHub Secretsにのみ保存します。

D1・R2・CDN設定はTerraformで「箱」を管理する

wrangler d1 createやwrangler r2 bucket createをその都度手で打つ代わりに、TerraformのcloudflareプロバイダーでD1データベース、R2バケット、CDN・SSL周りのゾーン設定を宣言的に管理できます。コードの内容は変わらないのにコマンドを打ち忘れる、というミスを防ぎやすくなります。

resource "cloudflare_d1_database" "app_db" {
  account_id = var.cloudflare_account_id
  name       = "my-app-db"
}

resource "cloudflare_r2_bucket" "uploads" {
  account_id = var.cloudflare_account_id
  name       = "my-app-uploads"
  location   = "APAC"
}

resource "cloudflare_zone_setting" "ssl" {
  zone_id    = var.cloudflare_zone_id
  setting_id = "ssl"
  value      = "strict"
}

resource "cloudflare_zone_setting" "always_https" {
  zone_id    = var.cloudflare_zone_id
  setting_id = "always_use_https"
  value      = "on"
}

terraform applyで作成したD1データベースのIDは出力(output)として取り出し、wrangler.jsoncのdatabase_idへ転記します。逆に言えば、Terraformが管理するのは「D1やR2という箱がある」ところまでで、その中のテーブル定義やコードは、これまでどおりwranglerとマイグレーションの仕事です。

ワークフローを分けて衝突を防ぐ:D1・R2・ゾーン設定はterraform apply用のワークフロー、アプリのコードはwrangler deploy用のワークフローと、GitHub Actions側でも別々のジョブ・別々のトリガーに分けます。同じリソースを2つのツールが同時に書き換えようとする状態を避けるのが目的です。細かい属性名はプロバイダーのバージョンで変わることがあるため、実装前にTerraform Registryの該当ページを確認してください。

Terraformのstateファイル自体の置き場所にも、非公開のR2バケットが使えます。R2はS3互換APIを持つため、Terraformのs3バックエンドをそのまま向けられます。

terraform {
  backend "s3" {
    bucket = "my-app-tfstate"
    key    = "cloudflare/terraform.tfstate"
    region = "auto"

    skip_credentials_validation = true
    skip_region_validation      = true
    skip_requesting_account_id  = true
    skip_metadata_api_check     = true
    skip_s3_checksum            = true
    use_path_style               = true

    endpoints = {
      s3 = "https://ACCOUNT_ID.r2.cloudflarestorage.com"
    }
  }
}
認証情報は最小権限で:このバックエンド用に、R2の当該バケットに対する読み書きだけを許可したAPIトークンを別途発行します。CloudflareアカウントAPIトークンを使い回さず、stateの読み書き専用に絞ります。stateにはリソースIDやシークレットの参照が含まれるため、バケットは前段のR2の説明どおり必ず非公開のままにし、公開用のカスタムドメインは絶対に紐づけません。

無料枠と制限を先に把握する

  • リクエスト数:無料プランは1日10万リクエストまで(商用利用も可)
  • CPU時間:無料プランはリクエストあたり10ms(一般的なアプリは2〜20ms程度を使用)
  • D1の容量:無料プランは500MB、有料プランは10GBが上限
  • D1の書き込み:秒間200件が目安の上限
  • R2の容量:無料プランは月10GBまで、転送量(エグレス)は無料

個人開発やプロトタイプなら十分な範囲ですが、成長を見込むサービスでは早い段階でこれらの上限を意識しておく必要があります。

自前サーバーとCloudflare Workers、どちらを選ぶか

Cloudflare Workersが向く

個人開発、検証段階のサービス、まずサーバー管理なしで公開したい場合。無料枠だけで商用サービスを試せます。

自前サーバーが向く

書き込みが多い業務システム、既存のDrizzle+PostgreSQL構成を維持したい場合、実績のある構成を優先したい場合。

どちらを選んでも、AIエージェントに実装を任せきりにせず、テストとデプロイ経路を明確にしてから運用する点は変わりません。

構成の選定から相談したい方へ

Cloudflare Workers、自前サーバーのどちらでも、要件に合わせて構築・運用まで対応します。

運用について相談する