2026年8月8日に実際にデプロイまで通した手順です。読み物ではなく作業手順書として書いてあります——上から順に実行してください。
踏んだ落とし穴は、それが起きるステップの中に埋め込んであります。後ろにまとめて置くと、詰まってから探すことになるからです。順序には依存関係があり、入れ替えると必ずどこかで失敗します。
| 表記 | 意味 |
|---|---|
| [ブラウザ] | Safari 等で画面を操作する。iPad で可 |
| [エージェント] | Claude Code に頼む。指示文をそのまま載せてある |
| [人] | 値を決める。エージェントに推測させない |
| ⚠ | そのステップで実際に踏んだ落とし穴。読み飛ばさない |
必要なもの:GitHub アカウント/Cloudflare アカウント/テンプレート化済みのリポジトリ(→Step 1)
| 項目 | バージョン | 確認したこと |
|---|---|---|
| Node | 22 | 20 だとデプロイ段だけが落ちる。下の注記を読むこと |
| wrangler | 4.120.0 | engines.node は >=22.0.0。deploy --dry-run は認証情報なしで完走する |
| @cloudflare/workers-types | 5.x | ^4 だと wrangler 4系の peer 依存と衝突して npm ci が落ちる。「wrangler 4 なら types も 4」は誤り |
| typescript | 5.x | tsc --noEmit が通る |
.nvmrc が 20 だと、Cloudflare のビルド環境が Node 20 を入れます。すると npm ci は警告で通ってしまうため、「ビルド中」まで緑で「デプロイ中」だけが数秒で赤くなります。
npm warn EBADENGINE package: 'wrangler@4.120.0',
npm warn EBADENGINE required: { node: '>=22.0.0' },
npm warn EBADENGINE current: { node: 'v20.20.2' }
教訓:.nvmrc と検証環境の Node が食い違っていると、その検証はビルド環境を代表しません。実際この雛形は公開前に npm install/tsc/--dry-run を通していましたが、検証環境が Node 22 だったため .nvmrc の 20 が一度も使われていませんでした。
| # | やること | 場所 | この順である理由 |
|---|---|---|---|
| 1 | テンプレートから複製 | ブラウザ | 空のリポジトリを作らない |
| 2 | 前のアプリの値を消す | エージェント | — |
| 3 | 作るものを決める | 人 | 4以降で使う値が要る |
| 4 | D1 を作る | ブラウザ | Worker より先。Worker は最初のデプロイ成功時に生まれる |
| 5 | wrangler.jsonc に実値 | エージェント | 4 の Database ID が要る |
| 6 | schema.sql を実行 | ブラウザ | 初回デプロイより前。無いと画面だけ500になる |
| 7 | Workers Builds に接続 | ブラウザ | — |
| 8 | main にマージ | エージェント | ここで初めて自動デプロイが走る |
| 9 | 動作を確認 | ブラウザ | 読み書きの両方 |
| 10 | main を保護 | ブラウザ | — |
Code タブ(Settings ではない)を開くUse this template → Create a new repositoryInclude all branches は Off のまま(main だけでよい。作業ブランチは不要)Create repositoryCLAUDE.md・雛形・CIワークフローが最初から入った状態で始まります。空のリポジトリだとエージェントは何の指示も持たない状態から始まり、毎回口頭で説明することになります。
Template repository にチェックを入れるだけです。以後 Use this template が出ます。複製先は履歴を引き継がず、コミット1本から始まります。
| 機械が戻すもの | 人が決めてエージェントが書き換えるもの |
|---|---|
wrangler.jsonc の name/database_name/database_id → change-mepackage.json の name/description複製元の環境記録( docs/SETUP.md 等)は削除 |
README.md … このアプリの説明にsrc/index.ts … 作るものに合わせてschema.sql … テーブル定義を作るものに合わせて |
| 項目 | 用途 | 制約 |
|---|---|---|
| アプリ名 | wrangler.jsonc の name。*.workers.dev のサブドメインにもなる | 小文字の英数字とハイフンのみ。大文字を含むと wrangler が弾く |
| 何をするアプリか | README.md/画面の見出し/src/index.ts の方針 | — |
| D1 データベース名 | wrangler.jsonc の database_name | Step 4 で作る名前と一致させる |
| 公開ドメイン | routes。指定なしなら <アプリ名>.<サブドメイン>.workers.dev | 独自ドメインは Cloudflare のゾーン配下に置く必要あり |
wrangler deploy は実行時に database_id を Cloudflare に問い合わせて解決します。D1 が存在しないと最初のデプロイが成功せず、Worker 自体が作られません。wrangler.jsonc に実値を入れるStep 3 と 4 で決まった値をエージェントに渡します。change-me は3か所です。
grep -rn 'change-me' wrangler.jsonc # 何も出ないこと
npm install # package-lock.json の name を同期
npm run check # 通ること
npm install を忘れない。package.json の name を変えると package-lock.json と食い違い、ビルド環境の npm ci が落ちます。
schema.sql をコンソールで実行するschema.sql の中身を貼って実行(複数文をまとめて受け付けます)コンピュート → Workers & Pages からリポジトリをインポートします。
| 項目 | 値 |
|---|---|
| Build command | npm ci |
| Deploy command | npx wrangler deploy |
| ルート ディレクトリ | / |
| Production branch | main |
| ビルド出力ディレクトリ | 空のまま。静的アセット用の項目で、Worker アプリでは使わない |
Only select repositories を選び、対象リポジトリだけに絞ること。この認可はAPI では代替できない唯一の関門ですが、アカウント/組織ごとに一度行えば以後は使い回されます。
main にマージする作業ブランチで開発 → npm run check が通ることを確認 → main へ。ここで初めて自動デプロイが走ります。
assets.directory エラー
[ERROR] The directory specified by the "assets.directory" field in your ...
/opt/buildhome/repo/dist
症状:クローン・インストール・ビルドは緑なのに、デプロイだけ失敗する。assets.directory は静的サイト版の設定。main が古い構成のまま接続すると、Cloudflare はそちらを読みます。audited 1 package(正しければ42前後)と、npx がその場で wrangler を取りに行っていること(正しければ npm ci で入っているので取りに行かない)。main に正しい構成をマージする。このステップを先に済ませれば起きません。
refusing to merge unrelated historiesmain が origin/main と無関係な履歴になっていることがあります。テンプレートから作ったリポジトリは履歴を引き継がないので起きやすい。
git checkout -B main origin/main
git merge --ff-only <作業ブランチ>
https://<アプリ名>.<サブドメイン>.workers.dev を開きます。
GET / と D1 の読み取りが通っているmain を保護するGitHub の Settings → Branches で main を保護し、build チェックの通過を必須にします。
main に入れません。設定はiPadのSafariから行えます。
Workers Builds のログは5段階に分かれます。どこが赤いかで原因の範囲が決まるので、まず段階を特定してください。ログを上から丁寧に読むより速い。
| 赤い段階 | 疑うもの |
|---|---|
| 初期化 | 接続設定そのもの |
| クローン | ブランチ指定、リポジトリの権限 |
| インストール | package-lock.json の不整合、依存の解決、workers-types のメジャー |
| ビルド | Build command そのもの |
| デプロイ | wrangler.jsonc の内容・Node のバージョン・D1 の実在 |
npm run check は tsc --noEmit と wrangler deploy --dry-run です。--dry-run は認証情報なしで動く代わりに、Cloudflare に問い合わせません。その線引きがそのまま限界になります。
| 分かる | 分からない |
|---|---|
| 型エラー | database_id が実在するか |
wrangler.jsonc の書式(Worker 名の規則違反など) | D1 にテーブルがあるか |
main が指すファイルの実在 | ビルド環境の Node で動くか |
| バインディングの「形」が解決すること | 実際にデプロイが通るか |
npm run check は間違いを早く見つけるための道具であって、正しさの証明ではありません。「手元が全部緑」はこの構成では出発点にすぎない。
src/index.ts には POST /api/items があり、これは誰でも書き込めます。経路確認の段階では問題ありませんが、URL が知られた状態で放置するとスパムを入れられます。
Step 3 で「何をするアプリか」を決めるときに、一緒に決めてください。
「とりあえず公開して後で塞ぐ」はしない——静的サイトが「書き込み経路を作らない」ことで安全を買っていたのに対し、こちらは書き込み経路がある前提で設計する必要があります。守るべきものが入れ替わっています。
.bind()——文字列連結で SQL を組み立てないawait req.json() の戻り値は unknown として扱い、型・長さ・必須を確認してから使う