Cloudflare WorkersにNext.jsアプリをデプロイする
はじめに
2024年9月にWorkers Static Assets機能がリリースされ、フルスタックなNext.jsアプリケーションがデプロイ可能になりました。現時点でまだ実験的機能の扱いですが、動作するか試してみます。
※CloudflareのドキュメントはNext.js v14で説明されているため、本記事もv14を前提に進めます。
プロジェクトを作成する
以下のコマンドを実行してプロジェクトを作成します。デプロイは後で行うため、「Do you want to deploy your application?」はNoと回答してください。
npm create cloudflare@latest my-next-app -- --framework=next --experimental
(補足)package.jsonについて
現時点では以下のpackage.jsonが作成されます。Cloudflareのドキュメントはpackage.jsonのscriptsを修正するように指示されていますが、修正は不要です。
package.jsonの内容
{
"name": "my-next-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"deploy": "opennextjs-cloudflare && wrangler deploy",
"preview": "opennextjs-cloudflare && wrangler dev",
"cf-typegen": "wrangler types --env-interface CloudflareEnv env.d.ts"
},
"dependencies": {
"next": "14.2.5",
"react": "^18",
"react-dom": "^18"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20250109.0",
"@opennextjs/cloudflare": "^0.3.7",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"eslint": "^8",
"eslint-config-next": "14.2.5",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5",
"wrangler": "^3.101.0"
}
}
デプロイする
ひとまずデプロイが成功するか確認します。以下のコマンドを実行してください。
npm run deploy
警告やエラーは発生せずデプロイは成功するはずです。ブラウザでhttps://my-next-app.<your_name>.workers.dev
を開いてみてください。
npm run deploy実行結果の例
参考までにnpm run deploy実行時のログを示します。
$ npm run deploy
> my-next-app@0.1.0 deploy
> opennextjs-cloudflare && wrangler deploy
┌─────────────────────────────┐
│ OpenNext — Cloudflare build │
└─────────────────────────────┘
App directory: /private/tmp/my-next-app
Next.js version : 14.2.5
OpenNext v3.3.1
┌─────────────────────────────────┐
│ OpenNext — Building Next.js app │
└─────────────────────────────────┘
> my-next-app@0.1.0 build
> next build
▲ Next.js 14.2.5
Creating an optimized production build ...
(node:50071) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
(node:50120) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
✓ Compiled successfully
✓ Linting and checking validity of types
✓ Collecting page data
✓ Generating static pages (5/5)
✓ Collecting build traces
✓ Finalizing page optimization
Route (app) Size First Load JS
┌ ○ / 5.23 kB 92.3 kB
└ ○ /_not-found 875 B 87.9 kB
+ First Load JS shared by all 87 kB
├ chunks/23-b75664ace61c0abb.js 31.5 kB
├ chunks/fd9d1056-2821b0f0cabcd8bd.js 53.7 kB
└ other shared chunks (total) 1.85 kB
○ (Static) prerendered as static content
┌──────────────────────────────┐
│ OpenNext — Generating bundle │
└──────────────────────────────┘
Bundling middleware function...
Bundling static assets...
Bundling cache assets...
Building server function: default...
# copyPackageTemplateFiles
⚙️ Bundling the OpenNext server...
# patchWranglerDeps
# updateWebpackChunksFile
- chunk 347.js
- chunk 682.js
- chunk 948.js
Applying code patches:
- patching require
- patching `buildId` function
- patching `loadManifest` function
- patching next's require
- patching `findDir` function
- patching `evalManifest` function
- patching cacheHandler
- patching 'require(this.middlewareManifestPath)'
- patching exception bubbling
- patching `loadInstrumentationModule` function
- patching `patchAsyncStorage` call
- patching `eval("require")` calls
- patching `require.resolve` call
All 13 patches applied
Worker saved in `/private/tmp/my-next-app/.open-next/worker.js` 🚀
OpenNext build complete.
⛅️ wrangler 3.101.0
--------------------
🌀 Building list of assets...
🌀 Starting asset upload...
🌀 Found 31 new or modified static assets to upload. Proceeding with upload...
+ /BUILD_ID
+ /_next/static/3IkHGSNZNy73_7lkbfk4L/_buildManifest.js
+ /_next/static/chunks/app/layout-cacac135ce820ae3.js
+ /next.svg
+ /_next/static/chunks/webpack-d0ceac4fb78a3613.js
+ /cnd-cgi/_next_cache/3IkHGSNZNy73_7lkbfk4L/_not-found.cache
+ /_next/static/media/26a46d62cd723877-s.woff2
+ /_next/static/media/55c55f0601d81cf3-s.woff2
+ /_next/static/media/6d93bde91c0c2823-s.woff2
+ /cnd-cgi/_next_cache/3IkHGSNZNy73_7lkbfk4L/favicon.ico.cache
+ /_next/static/chunks/fd9d1056-2821b0f0cabcd8bd.js
+ /_next/static/3IkHGSNZNy73_7lkbfk4L/_ssgManifest.js
+ /_next/static/chunks/pages/_error-1be831200e60c5c0.js
+ /_next/static/chunks/main-app-8cc085c9d52c8c97.js
+ /_next/static/chunks/app/_not-found/page-05886c10710171db.js
+ /_next/static/media/df0a9ae256c0569c-s.woff2
+ /_next/static/chunks/173-32f9ff9bdfb525b3.js
+ /_next/static/media/581909926a08bbc8-s.woff2
+ /favicon.ico
+ /_next/static/chunks/polyfills-78c92fac7aa8fdd8.js
+ /_next/static/chunks/23-b75664ace61c0abb.js
+ /_next/static/chunks/app/page-41ceab464b24ef75.js
+ /_next/static/chunks/pages/_app-6a626577ffa902a4.js
+ /vercel.svg
+ /cnd-cgi/_next_cache/3IkHGSNZNy73_7lkbfk4L/500.cache
+ /_next/static/media/97e0cb1ae144a2a9-s.woff2
+ /_next/static/css/f451d9dd0f2d5d98.css
+ /cnd-cgi/_next_cache/3IkHGSNZNy73_7lkbfk4L/index.cache
+ /_next/static/media/a34f9d1faa5f3315-s.p.woff2
+ /_next/static/chunks/main-c8553ba68b419844.js
+ /_next/static/chunks/framework-f66176bb897dc684.js
Uploaded 10 of 31 assets
Uploaded 20 of 31 assets
Uploaded 31 of 31 assets
✨ Success! Uploaded 31 files (6.85 sec)
Total Upload: 5238.56 KiB / gzip: 1355.74 KiB
Worker Startup Time: 92 ms
Uploaded my-next-app (23.24 sec)
Deployed my-next-app triggers (0.90 sec)
https://my-next-app.moutend.workers.dev
Current Version ID: 35c1e4fb-0e51-43ea-92ed-4da5154b8b41
サーバーサイドコンポーネントを実装する
Workersで動的にコンテンツをレンダリングできることを確認するため、大吉・中吉・小吉をランダムに返すおみくじコンポーネントを実装してみます。
実装するのは以下の2つです。
- src/components/Omikuji.tsx
- src/app/omikuji/page.tsx
※プロジェクト初期化時に作成されるtsconfig.jsonにpathの設定がされているため、上記2つのファイルを作成するだけで動作します。
src/components/Omikuji.tsx
src/componentsディレクトリを作成し、以下の内容でOmikuji.tsxを作成します。
export default function Omikuji() {
const results = ['大吉', '中吉', '小吉'];
const randomIndex = Math.floor(Math.random() * results.length);
const fortune = results[randomIndex];
return (
<div>
<p>あなたの運勢は…</p>
<p style={{ fontSize: '2rem', fontWeight: 'bold' }}>{fortune}</p>
</div>
);
}
src/app/omikuji/page.tsx
src/app/omikujiディレクトリを作成し、以下の内容でpage.tsxを作成します。
import { unstable_noStore } from "next/cache";
import Omikuji from "@/components/Omikuji";
export default function OmikujiPage() {
// サーバーサイドでおみくじを引く。
// Cloudflare WorkersがNext.js v15に対応したらunstable_noStore()からconnection()に変更する。
// await connection();
unstable_noStore();
return (
<main style={{ padding: '1rem' }}>
<h1>2025年の運勢は?</h1>
<p>
新しい一年が始まりましたね。運勢をチェックして、良いスタートを切りましょう!
</p>
<Omikuji />
</main>
);
}
動作確認
デプロイしましょう。
npm run deploy
その後、ブラウザでhttps://my-next-app.<your_name>.workers.dev/omikuji
を開きます。リロードするたびに大吉・中吉・小吉がランダムに表示されます。
バインディングを利用する
WorkersのバインディングはgetCloudflareContext()
で参照できます。試してみましょう。
以下の機能を実装することにします。
- D1データベースを作成し、omikuji_historyテーブルを定義する。
- サーバーサイドでリクエストの都度ランダムに大吉・中吉・小吉を選ぶ。
- 結果をomikuji-historyテーブルにINSERTする。
- 直近の10件をSELECTする。
- SELECTした結果を
<table>
形式でレスポンスする。
なお、実装にはDrizzle ORMを利用します。素のD1バインディングで読み書きしても構わないのですが、結果をTypeScriptのオブジェクトにマッピングするのが手間なのでORMを利用します。Drizzle ORMの使い方については以下の記事も参考にしてください。
Cloudflare D1でDrizzle ORMを使う - Zenn
Drizzle ORMをインストールする
以下のコマンドを実行します。
npm i drizzle-orm dotenv
npm i -D drizzle-kit tsx
drizzle.config.tsを作成する
プロジェクトのルートにdrizzle.config.ts
を作成します。以下の内容でファイルを作成してください。
import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
out: './drizzle',
schema: './src/db/schema.ts',
dialect: 'sqlite',
driver: 'd1-http',
dbCredentials: {
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
token: process.env.CLOUDFLARE_D1_TOKEN!,
},
});
D1データベースを作成する
以下のコマンドを実行します。データベースの名前は適当で構いません。
npx wrangler d1 create my-database
wrangler.tomlファイルを編集する
D1バインディングの設定を追加します。
#:schema node_modules/wrangler/config-schema.json
name = "my-next-app"
main = ".open-next/worker.js"
compatibility_date = "2024-09-26"
compatibility_flags = ["nodejs_compat"]
# Minification helps to keep the Worker bundle size down and improve start up time.
minify = true
# Use the new Workers + Assets to host the static frontend files
assets = { directory = ".open-next/assets", binding = "ASSETS" }
# ここから追加
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
型定義を生成する
以下のコマンドを実行します。
npm run cf-typegen
.envに環境変数を設定する
プロジェクトのルートに.env
を配置します。以下の内容でファイルを作成してください。
CLOUDFLARE_ACCOUNT_ID='CloudflareのアカウントID'
CLOUDFLARE_DATABASE_ID='wrangler.tomlファイルに設定したdatabase_id'
CLOUDFLARE_D1_TOKEN='Cloudflareダッシュボードで発行したAPIトークン'
テーブルを定義する
src/dbディレクトリを作成し、以下の内容でsrc/db/schema.tsを作成します。
import { sqliteTable, int, text } from "drizzle-orm/sqlite-core";
export const omikujiHistoryTable = sqliteTable("omikuji_history", {
id: int().primaryKey({ autoIncrement: true }),
fortune: text().notNull(),
createdAt: text("created_at").notNull(),
});
マイグレーションを実行する
以下のコマンドを実行します。
npx drizzle-kit generate
npx drizzle-kit migrate
src/app/d1/page.tsxを実装する
import { unstable_noStore } from "next/cache";
import { getCloudflareContext } from "@opennextjs/cloudflare";
import { drizzle } from "drizzle-orm/d1";
import { desc } from "drizzle-orm";
import { omikujiHistoryTable } from "@/db/schema";
async function insert(env: CloudflareEnv, fortune: string) {
const db = drizzle(env.DB);
const record: typeof omikujiHistoryTable.$inferInsert = {
fortune,
createdAt: new Date().toISOString(),
};
await db.insert(omikujiHistoryTable).values(record);
}
async function select(
env: CloudflareEnv,
): Promise<(typeof omikujiHistoryTable.$inferSelect)[]> {
const db = drizzle(env.DB);
const result = await db
.select()
.from(omikujiHistoryTable)
.orderBy(desc(omikujiHistoryTable.id))
.limit(10);
return result;
}
export default async function Page() {
unstable_noStore();
const results = ["大吉", "中吉", "小吉"];
const randomIndex = Math.floor(Math.random() * results.length);
const fortune = results[randomIndex];
const { env } = await getCloudflareContext();
await insert(env, fortune);
const history = await select(env);
return (
<div>
<h1>おみくじの履歴</h1>
<table border={1}>
<thead>
<tr>
<th>ID</th>
<th>Fortune</th>
<th>Created At</th>
</tr>
</thead>
<tbody>
{history.map((record) => (
<tr key={record.id}>
<td>{record.id}</td>
<td>{record.fortune}</td>
<td>{new Date(record.createdAt).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
動作確認
デプロイしましょう。
npm run deploy
ブラウザでhttps://my-next-app.<your_name>workers.dev/d1
を開くとおみくじの履歴が表示されます。何度かリロードして、直近10件のレコードが取得されていることを確かめてみてください。
おわりに
軽く試す程度であれば特に不具合なく実装できました。ただし、現時点では実験的機能の扱いです。本番環境で利用するにはしばらく様子見ですね。
Discussion