自分でサーバーを持たない、という選択
この手引の基本構成は、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 deploywrangler loginで認証した状態であれば、この1コマンドでビルドからCloudflare Workersへのデプロイまで完了します。D1やKVなどのバインディングは、サーバー側のコードからcloudflare:workers経由で読み込みます。
D1(データベース)とR2(ファイル保存)を使い分ける
Cloudflare D1はSQLiteをベースにしたデータベースで、Workersから直接呼び出せます。Sessions APIによるグローバルなリードレプリケーションにも対応し、世界複数リージョンへ読み取りを分散できます。ただし書き込みは1つのプライマリに集約される仕組みのままで、目安として秒間200件を超える書き込みが続くとロック待ち(SQLITE_BUSY)が発生し始めます。
画像やPDFなどのファイルはD1ではなくR2(オブジェクトストレージ)に保存します。S3互換のAPIを持ち、最大の特徴は転送量(エグレス)に課金されないことです。ユーザーがアップロードした画像を配信するようなNext.jsアプリでは、D1に構造化データ、R2にファイル本体という役割分担が基本になります。
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: deploywrangler 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とマイグレーションの仕事です。
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"
}
}
}無料枠と制限を先に把握する
- リクエスト数:無料プランは1日10万リクエストまで(商用利用も可)
- CPU時間:無料プランはリクエストあたり10ms(一般的なアプリは2〜20ms程度を使用)
- D1の容量:無料プランは500MB、有料プランは10GBが上限
- D1の書き込み:秒間200件が目安の上限
- R2の容量:無料プランは月10GBまで、転送量(エグレス)は無料
個人開発やプロトタイプなら十分な範囲ですが、成長を見込むサービスでは早い段階でこれらの上限を意識しておく必要があります。
自前サーバーとCloudflare Workers、どちらを選ぶか
Cloudflare Workersが向く
個人開発、検証段階のサービス、まずサーバー管理なしで公開したい場合。無料枠だけで商用サービスを試せます。
自前サーバーが向く
書き込みが多い業務システム、既存のDrizzle+PostgreSQL構成を維持したい場合、実績のある構成を優先したい場合。
どちらを選んでも、AIエージェントに実装を任せきりにせず、テストとデプロイ経路を明確にしてから運用する点は変わりません。