1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

フルスタックCloudflareでPayload CMSをお試しデプロイ(サーバーレスでCMS)

1
Posted at

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は以下の通りです。

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データベースの情報で修正します。

wrangler.jsonc
  "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バケットの情報で修正します。

wrangler.jsonc
  "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

コマンド実行前(抜粋)

cloudflare-env.d.ts
/* 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 {}

コマンド実行後(抜粋)

cloudflare-env.d.ts
/* 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を修正します。

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データベース名使用箇所を修正します。

package.json
-    "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のコンソールでデプロイ状況を確認できればデプロイ成功です。

スクリーンショット 2025-11-29 15.46.27.png

デプロイの確認

トップページと初期ログインページ(/admin/create-first-user)の表示は確認できました。
スクリーンショット 2025-11-29 15.45.24.png
スクリーンショット 2025-11-29 15.45.27.png

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?