Payload CMSがCloudFlareのデプロイに対応しました。
Payload CMSを触ったことがありませんが、サーバーレスしかもCloudFlareでCMSを動かせそうなため、お試しでCloudFlareにデプロイしてみました。
テンプレートのおかげで、ランタイムはWorkers、DBはD1、ストレージはR2とフルスタックCloudFlareで始めやすくなっています。
注意としてWorkers Paidのプラン($5およそ780円)への変更が必要です。バンドルサイズが3MBを超えるためです。
※私はNext.jsの知識はほとんど持っていません。ご了承ください。
Payload CMSとは
オープンソースのヘッドレスCMSです。ヘッドレスCMSのため管理画面しかなく、フロントエンドは自前で用意する必要があります。
バージョン3.0からNext.jsベースになったようです。以前はExpress + Reactベースのようでした。
https://payloadcms.com/posts/blog/payload-30-the-first-cms-that-installs-directly-into-any-nextjs-app
https://payloadcms.com/posts/blog/should-we-move-to-nextjs
環境
% sw_vers
ProductName: macOS
ProductVersion: 26.1
BuildVersion: 25B78
% node -v
v22.19.0
% pnpm -v
10.23.0
テンプレートの作成
Payload CMSのクイックスタートを実行しテンプレートを作成します。
Node.jsのパッケージマネージャーは今回はpnpmを使います。
Installation | Documentation | Payload
% pnpx create-payload-app
プロジェクト名は「my-app」とします。
┌ create-payload-app
│
◇ ────────────────────────────────────────────╮
│ │
│ Welcome to Payload. Let's create a project! │
│ │
├───────────────────────────────────────────────╯
│
◆ Project name?
│ my-app
└
「with-cloudflare-d1」テンプレートを選択します。
◆ Choose project template
│ ○ blank
│ ○ website
│ ○ ecommerce
│ ● with-cloudflare-d1
│ ○ plugin
└
「Cloudflare D1 SQlite」データベースを選択します。
◆ Select a database
│ ● Cloudflare D1 SQlite
│ ○ MongoDB
│ ○ PostgreSQL
│ ○ SQLite
│ ○ Vercel Postgres
└
クイックスタートが完了後「my-app」ディレクトリにテンプレートが生成されます。
「my-app」ディレクトリをお好きなエディタなどで開きます。
wranglerの認証
wranglerを認証しCloudFlareのリソースを操作できるようにします。
% pnpm wrangler login
D1データベースとR2バケットを作成
デフォルトのwrangler.jsoncは以下の通りです。
{
"$schema": "node_modules/wrangler/config-schema.json",
"main": ".open-next/worker.js",
"name": "my-app",
"compatibility_date": "2025-08-15",
"compatibility_flags": [
// Enable Node.js API
// see https://developers.cloudflare.com/workers/configuration/compatibility-flags/#nodejs-compatibility-flag
"nodejs_compat",
// Allow to fetch URLs in your app
// see https://developers.cloudflare.com/workers/configuration/compatibility-flags/#global-fetch-strictly-public
"global_fetch_strictly_public",
],
"assets": {
"directory": ".open-next/assets",
"binding": "ASSETS",
},
"d1_databases": [
{
"binding": "D1",
"database_id": "DATABASE_ID",
"database_name": "my-app",
"remote": true,
},
],
"services": [
// The service should match the "name" of your worker
// {
// "binding": "WORKER_SELF_REFERENCE",
// "service": "my-app"
// },
],
"r2_buckets": [
{
"binding": "R2",
"bucket_name": "my-app",
},
// Create a R2 binding with the binding name "NEXT_INC_CACHE_R2_BUCKET"
// {
// "binding": "NEXT_INC_CACHE_R2_BUCKET",
// "bucket_name": "<BUCKET_NAME>",
// },
],
// Here's how to configure an additional environment
// It can be deployed with `CLOUDFLARE_ENV=staging pnpm run deploy`
// "env": {
// "staging": {
// "name": "my-app-staging",
// "d1_databases": [
// {
// "binding": "D1",
// "database_id": "DATABASE_ID",
// "database_name": "my-app-staging",
// "remote": true
// }
// ],
// "services": [
// // The service should match the "name" of your worker
// // {
// // "binding": "WORKER_SELF_REFERENCE",
// // "service": "my-app-staging"
// // },
// ],
// "r2_buckets": [
// {
// "binding": "R2",
// "bucket_name": "my-app-staging"
// }
// // Create a R2 binding with the binding name "NEXT_INC_CACHE_R2_BUCKET"
// // {
// // "binding": "NEXT_INC_CACHE_R2_BUCKET",
// // "bucket_name": "<BUCKET_NAME>",
// // },
// ]
// }
// }
}
d1_databasesとr2_bucketsはダミーの情報が記載されています。
D1データベースとR2バケットを作成し、CloudFlare実環境の情報を記載します。
D1データベースの作成
名前は「my-app-db」としてD1データベースを作成します。
Wrangler commands · Cloudflare D1 docs
% pnpm wrangler d1 create my-app-db
⛅️ wrangler 4.46.0 (update available 4.51.0)
─────────────────────────────────────────────
✅ Successfully created DB 'my-app-db' in region APAC
Created your new D1 database.
To access your new D1 Database in your Worker, add the following snippet to your configuration file:
{
"d1_databases": [
{
"binding": "my_app_db",
"database_name": "my-app-db",
"database_id": "(払い出されたdatabase_id)"
}
]
}
wrangler.jsoncに自動で追加しますかと聞かれます。今回は「n」を入力してnoとしました。
? Would you like Wrangler to add it on your behalf? … no
D1データベースの情報の記載
wrangler.jsoncを生成したD1データベースの情報で修正します。
"d1_databases": [
{
- "binding": "D1",
- "database_id": "DATABASE_ID",
- "database_name": "my-app",
+ "binding": "my_app_db",
+ "database_id": "(払い出されたdatabase_id)",
+ "database_name": "my-app-db",
"remote": true,
},
],
R2バケットの作成
名前は「my-app-media」としてR2バケットを作成します。
Create new buckets · Cloudflare R2 docs
% pnpm wrangler r2 bucket create my-app-media
⛅️ wrangler 4.46.0 (update available 4.51.0)
─────────────────────────────────────────────
Creating bucket 'my-app-media'...
✅ Created bucket 'my-app-media' with default storage class of Standard.
To access your new R2 Bucket in your Worker, add the following snippet to your configuration file:
{
"r2_buckets": [
{
"bucket_name": "my-app-media",
"binding": "my_app_media"
}
]
}
D1データベース同様に「n」を入力しました。
✔ Would you like Wrangler to add it on your behalf? … no
R2バケットの情報の記載
wrangler.jsoncを生成したR2バケットの情報で修正します。
"r2_buckets": [
{
- "binding": "R2",
- "bucket_name": "my-app",
+ "binding": "my_app_media",
+ "bucket_name": "my-app-media",
},
cloudflare-env.d.ts(Bindingsの型)の再生成
D1データベースとR2バケットの名前を変更したため、Bindngsの型を再生成します。
Bindingsの型はcloudflare-env.d.tsに記載されています。
テンプレートのpackage.jsonのscriptsに記載のコマンドを実行します。
% pnpm run generate:types:cloudflare
コマンド実行前(抜粋)
/* eslint-disable */
// Generated by Wrangler by running `wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts` (hash: 77b2af8ebc9b6da890235d525e4224d0)
// Runtime types generated with workerd@1.20251105.0 2025-08-15 global_fetch_strictly_public,nodejs_compat
declare namespace Cloudflare {
interface Env {
R2: R2Bucket
D1: D1Database
ASSETS: Fetcher
}
}
interface CloudflareEnv extends Cloudflare.Env {}
コマンド実行後(抜粋)
/* eslint-disable */
// Generated by Wrangler by running `wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts` (hash: 5abb5b5b0541f92879474d1815d3e121)
// Runtime types generated with workerd@1.20251105.0 2025-08-15 global_fetch_strictly_public,nodejs_compat
declare namespace Cloudflare {
interface Env {
PAYLOAD_SECRET: string;
my_app_media: R2Bucket;
my_app_db: D1Database;
ASSETS: Fetcher;
}
}
interface CloudflareEnv extends Cloudflare.Env {}
type StringifyValues<EnvType extends Record<string, unknown>> = {
[Binding in keyof EnvType]: EnvType[Binding] extends string ? EnvType[Binding] : string;
};
declare namespace NodeJS {
interface ProcessEnv extends StringifyValues<Pick<Cloudflare.Env, "PAYLOAD_SECRET">> {}
}
R2BucketとD1Databaseのプロパティ名が変わりました。
プロジェクト直下に.envファイルが存在するため、.envの内容(PAYLOAD_SECRET)も追加されていました。
src/payload.config.tsの修正
cloudflare-env.d.tsが変更されたため、cloudflare-env.d.tsを参照しているsrc/payload.config.tsを修正します。
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
-db: sqliteD1Adapter({ binding: cloudflare.env.D1 }),
+db: sqliteD1Adapter({ binding: cloudflare.env.my_app_db }),
plugins: [
// storage-adapter-placeholder
r2Storage({
- bucket: cloudflare.env.R2,
+ bucket: cloudflare.env.my_app_media,
collections: { media: true },
}),
],
※ "db"のインデントが足りませんが、テンプレートの状態からこのような状態のため、今回は修正していません。
package.jsonの修正
package.jsonのD1データベース名使用箇所を修正します。
- "deploy:database": "cross-env NODE_ENV=production PAYLOAD_SECRET=ignore payload migrate && wrangler d1 execute D1 --command 'PRAGMA optimize' --env=$CLOUDFLARE_ENV --remote",
+ "deploy:database": "cross-env NODE_ENV=production PAYLOAD_SECRET=ignore payload migrate && wrangler d1 execute my-app-db --command 'PRAGMA optimize' --env=$CLOUDFLARE_ENV --remote",
CloudFlareへデプロイ
テンプレートのpackage.jsonのscriptsに記載のコマンドを実行します。
% pnpm run deploy
deployはdeploy:databaseとdeploy:appが実行されるようになっています。
deploy:databaseはD1データベースに対してマイグレーションを実行、deploy:appはソースのビルドとデプロイを実行します。
実行結果にエラーがなく、CloudFlareのコンソールでデプロイ状況を確認できればデプロイ成功です。
デプロイの確認
トップページと初期ログインページ(/admin/create-first-user)の表示は確認できました。


デプロイと少しの動作確認までできました。
バンドルサイズが大きいためWorkers Paidにプラン変更して$5の課金が必要です。しかし、
他のCMSのホスティング方法よりだいぶ安いと思います。それに、CloudFlareの十分なクォータを活かせたり、CloudFlareの他のサービスとの連携もしやすいです。
Payload CMSはバンドルサイズの削減に取り組んでいるとのことで期待しています。
