はじめに:作れたのに、人に見せられない
生成AIに頼んで、アプリの形になるところまでは進んだ。手元のパソコンでは動いている。けれど、そこから先へ進めない。
このようにつまずく方は、多いかと思います。ChatGPTやClaudeに指示を出せば、動くコードは出てきます。ところが出てきたコードは、あなたのパソコンの中でしか動きません。
そして公開方法を調べ始めると、サーバー、ドメイン、SSL、デプロイといった単語が一斉に出てきて、そこで止まります。
図1:同じコードでも、置き場所を変えると「誰が開けるか」が変わります。右側では、あなたのパソコンの電源とアプリの稼働が切り離されている点に注目してください。
この記事は、その公開の手順を扱います。生成AIに作ってもらったアプリを、Cloudflareに置いて、公開URLを手に入れるまでです。
対象は次のような方です。
- 生成AIでアプリらしきものは作れたが、公開の仕方が分からない方
- 「サーバーを借りる」の時点で気後れしている方
- 無料で試したいが、うっかり課金されるのが怖い方
プログラムを書いた経験は問いません。ターミナル(黒い画面にコマンドを打つアプリ)を開いて、指示どおりに文字を打ち込めれば進められます。
この記事で作るもの
説明を具体的にするため、読書メモアプリを題材にします。本のタイトルと感想を入力して保存し、あとから一覧で見返せる、それだけのアプリです。
小さく見えますが、公開に必要な要素はひととおり入っています。
| 要素 | 中身 | 生成AIが作る部分 |
|---|---|---|
| 画面 | 入力フォームと一覧表示 | HTMLとJavaScript |
| 処理 | 保存する、一覧を返す | サーバー側のコード |
| 保存場所 | メモを残しておく場所 | データベースの定義 |
この3つを1か所に置いて公開する、というのがこの記事の内容です。
なぜ、生成AI製アプリの公開先にCloudflareが選ばれるのか
理由は4つあります。順に見ていきます。
理由1:公開のためにやることが、2つしかない
自分でサーバーを1台用意する場合、公開までにやることは、
サーバーを借りる、OSを設定する、Webサーバーを入れる、データベースを入れる、SSL証明書を取る、アプリを転送して起動する・・・
これだけあります。
Cloudflareの場合、コマンドは2つです。ひな型を作るコマンドと、公開するコマンド。それだけです。
図2:工程の数の差です。上段は公開したあともOSの更新や証明書の更新が続きますが、下段はその土台をCloudflare側が持ってくれます。
差が生まれるのは、土台の管理を誰が持つかが違うからです。Cloudflareでは、OSもWebサーバーも証明書もCloudflare側にあり、あなたは自分のコードだけを預けます。この方式をサーバーレスと呼びます。サーバーが無いという意味ではなく、あなたが世話をするサーバーが無い、という意味です。
理由2:画面もAPIもデータも、1つのWorkerに収まる
Cloudflareでプログラムを動かす仕組みをWorkers(ワーカーズ) と呼びます。ここが他のサービスと大きく違うところなのですが、画面のファイル配信、処理の実行、データベースへの接続が、1つのWorkerの中に同居します。
図3:ブラウザからの要求は、行き先によって受け取り手が変わります。/ は静的アセットが、/api/notes はあなたのコードが受け取ります。
一般的な構成では、画面の置き場所、処理を動かす場所、データベースを別々に契約し、それぞれをつなぎ込む設定が要ります。この「つなぎ込み」が初心者の最大の関門です。Cloudflareでは、設定ファイルに数行書くだけで、データベースを呼び出せるようになります。この仕組みを binding(バインディング) と呼びます。
生成AIに「1つのフォルダで完結するアプリを作って」と頼んだときに出てくる構成と、Cloudflareの構成が素直に噛み合う。これが2つ目の理由です。
理由3:無料枠が「置きっぱなしにできる」量
ここは誤解が多いところなので、丁寧に説明します。
Cloudflareの無料枠は、すべてのアクセスを数えているわけではありません。
図4:画面まわりのファイル配信は無料枠を減らしません。無料枠を使うのは、あなたのコードが動いたときだけです。
公式ドキュメントには、静的アセットへのリクエストについて "Requests to static assets are free and unlimited"(静的アセットへのリクエストは無料かつ無制限)と明記されています。つまり、HTMLやCSSや画像を何回配信しても、無料枠は減りません。
数えられるのは、あなたのコードが実行された回数です。無料プランの上限は次のとおりです(2026年9月時点)。
| 数えられるもの | 無料プランの上限 |
|---|---|
| Workersの実行 | 1日10万回 |
| 1回あたりのCPU時間 | 10ミリ秒 |
| D1の読み取り | 1日500万行 |
| D1の書き込み | 1日10万行 |
| D1の保存容量 | 合計5GB |
クレジットカードの登録も不要です。作ったものを公開したまま置いておいて、月末に請求が来る心配をしなくていい。試作を公開して人に見せる、という用途で使用される方に向いています。
理由4:生成AI自身が、Cloudflareの書き方を知っている
Cloudflareは、AIコーディングツール向けの窓口を公式に用意しています。ドキュメント全体をAIが読みやすい形にまとめた llms.txt、公式のMCPサーバー、そしてClaude CodeやCodex、Cursor向けのセットアップ手順です。
Claude Codeであれば、次の2行を実行するだけで、Cloudflare公式のスキル一式が入ります。
/plugin marketplace add cloudflare/skills
/plugin install cloudflare@cloudflare
これを入れておくと、npx wrangler deploy や wrangler d1 migrations apply といったコマンドを、AIが自分で必要な場面で選ぶようになります。「公開して」と頼めば、公開までの手順をAIが組み立ててくれるということです。
生成AIでアプリを作る人にとって、AIが正確な情報を持っている環境かどうかは、実際の作業時間に直結します。
準備するもの
必要なものは3つだけです。
| 必要なもの | 用意の仕方 | 費用 |
|---|---|---|
| Cloudflareのアカウント | メールアドレスで登録 | 無料。カード登録も不要 |
| Node.js | 公式サイトからLTS版を入れる | 無料 |
| ターミナル | Macは「ターミナル」、Windowsは「PowerShell」 | 標準で入っています |
Node.jsが入っているかどうかは、ターミナルで次を実行すると分かります。
node -v
v22.x.x のようにバージョンが表示されれば入っています。command not found と出たら、Node.js公式サイトからLTS版を入れてください。
公開までの6手順
全体の流れを先に示します。
図5:手順3から5はアプリの中身づくりで、生成AIに任せられる部分です。Cloudflare固有の作業は1・2・6の3つだけです。
以下のコマンドは、Cloudflare公式ガイドの手順に沿って、読書メモアプリ向けに書き換えたものです。
表示される内容はツールのバージョンによって変わります。画面に出る案内も読みながら進めてください。
手順1. アカウントを作る
Cloudflareのサイトでメールアドレスとパスワードを登録します。無料プランのままならクレジットカードの登録画面は出てきません。
登録が終わったら、ターミナルからログインしておきます。ブラウザが開いて承認を求められるので、許可してください。
npx wrangler login
wrangler(ラングラー)は、Cloudflareの操作をコマンドで行うための道具です。次の手順で自動的に入ります。
手順2. ひな型を作る
作業したいフォルダに移動して、次を実行します。
npm create cloudflare@latest -- book-notes
対話形式でいくつか聞かれます。この記事の流れに合わせるなら、次のように答えてください。
| 質問 | 選ぶもの |
|---|---|
| What would you like to start with? | Hello World example |
| Which template would you like to use? | Worker only |
| Which language do you want to use? | TypeScript |
| Do you want to use git for version control? | Yes |
| Do you want to deploy your application? | No |
最後の「今すぐ公開しますか」には、いったん No と答えます。中身を作ってから公開したいからです。
終わったら、できたフォルダに移動します。
cd book-notes
手順3. 画面を置く
生成AIに作ってもらったHTMLを置く場所を作ります。フォルダ名は public にします。
mkdir public
public/index.html を作り、次の内容を入れます。読書メモの入力フォームと一覧表示だけの、最小の画面です。
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>読書メモ</title>
</head>
<body>
<h1>読書メモ</h1>
<form id="form">
<input id="title" placeholder="本のタイトル" required />
<textarea id="comment" placeholder="感想"></textarea>
<button type="submit">保存する</button>
</form>
<ul id="list"></ul>
<script>
const list = document.getElementById("list");
async function load() {
const res = await fetch("/api/notes");
const notes = await res.json();
list.replaceChildren(
...notes.map((n) => {
const li = document.createElement("li");
const strong = document.createElement("strong");
strong.textContent = n.title;
li.append(strong, document.createElement("br"), n.comment);
return li;
}),
);
}
document.getElementById("form").addEventListener("submit", async (e) => {
e.preventDefault();
await fetch("/api/notes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: document.getElementById("title").value,
comment: document.getElementById("comment").value,
}),
});
e.target.reset();
load();
});
load();
</script>
</body>
</html>
一覧の描画に innerHTML を使わず、textContent と createElement で組み立てている点に注意してください。生成AIは innerHTML を使った短いコードを出しがちですが、それだと本のタイトルにHTMLタグを書き込まれたときに、そのまま画面で実行されてしまいます。他人にURLを渡すなら、ここは最初から直しておく価値があります。
次に、この public フォルダを画面として配信するよう設定します。プロジェクト直下の wrangler.jsonc を開き、assets の項目を足します。
{
"name": "book-notes",
"main": "src/index.ts",
"compatibility_date": "2026-09-01",
"assets": {
"directory": "./public"
}
}
この2行を足すだけで、public の中身がそのまま画面として配信されるようになります。ファイルが見つかった要求は画面が返し、見つからなかった要求だけがあなたのコードに回ります。図3で説明した振り分けは、この設定によるものです。
新しいバージョンのひな型では、最初から assets が書かれていることがあります。その場合は directory の値だけ確認してください。
手順4. APIを書く
src/index.ts を、次の内容に置き換えます。一覧を返す入口と、1件保存する入口の2つです。
export interface Env {
// 設定ファイルの binding に書いた名前と必ずそろえます
DB: D1Database;
}
export default {
async fetch(request, env): Promise<Response> {
const url = new URL(request.url);
// メモの一覧を返す
if (url.pathname === "/api/notes" && request.method === "GET") {
const { results } = await env.DB.prepare(
"SELECT id, title, comment, created_at FROM notes ORDER BY id DESC LIMIT 50",
).all();
return Response.json(results);
}
// メモを1件保存する
if (url.pathname === "/api/notes" && request.method === "POST") {
const body = (await request.json()) as { title?: string; comment?: string };
const title = (body.title ?? "").trim();
const comment = (body.comment ?? "").trim();
if (!title) {
return Response.json({ error: "タイトルは必須です" }, { status: 400 });
}
await env.DB.prepare("INSERT INTO notes (title, comment) VALUES (?, ?)")
.bind(title, comment)
.run();
return Response.json({ ok: true }, { status: 201 });
}
return new Response("Not Found", { status: 404 });
},
} satisfies ExportedHandler<Env>;
ここで押さえておきたいのは、SQLの中に入力値を直接つなげていない点です。? を置いて bind() で渡しています。文字列をそのままつなげると、入力欄に細工をされてデータを消されたり抜かれたりするSQLインジェクションという攻撃を受けます。bind() がその防止策です。
生成AIはここを文字列連結で書いてくることがあります。prepare の中に ${} が入っていたら、? と bind() に直してください。
手順5. D1をつなぐ
D1は、Cloudflareが用意しているデータベースです。表計算ソフトの表をイメージしてください。1件保存するたびに行が増えていきます。
まずデータベースを作ります。
npx wrangler d1 create book-notes-db
実行すると、設定に貼り付けるための情報(データベースID)が表示されます。設定ファイルへ追記するか聞かれたら、追記してもらうのが簡単です。手で書く場合は wrangler.jsonc を次のようにします。
{
"name": "book-notes",
"main": "src/index.ts",
"compatibility_date": "2026-09-01",
"assets": {
"directory": "./public"
},
"d1_databases": [
{
"binding": "DB",
"database_name": "book-notes-db",
"database_id": "<表示されたIDをここに貼る>"
}
]
}
binding に書いた名前が、コードの中でデータベースを指す名前になります。ここでは DB としたので、コードからは env.DB で呼び出せます。手順4のコードと同じ名前になっているか、いま確認しておいてください。
この名前の食い違いは、あとで必ず出てくるつまずきどころです。設定ファイルとコードの両方を見比べてください。
次に、表の形を書いた schema.sql をプロジェクト直下に作ります。
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
comment TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- 新しい順に並べる検索を速くするための索引
CREATE INDEX IF NOT EXISTS idx_notes_created ON notes (created_at);
CREATE TABLE IF NOT EXISTS にしているのは、何度実行しても既存のデータを壊さないためです。ネット上のサンプルには先頭に DROP TABLE(表を削除)が入っているものがありますが、それを公開後の本番に流すと、保存済みのメモが消えます。
手順6. 手元で確かめて、公開する
まず手元のデータベースに表を作ります。
npx wrangler d1 execute book-notes-db --local --file=./schema.sql
手元で起動して、画面を確認します。
npx wrangler dev
表示されたURL(多くは http://localhost:8787)を開き、メモを1件保存してから、再読み込みして残っていれば成功です。
動いたら、本番側にも同じ表を作ります。ここを忘れる人が多いので、コマンドをよく見てください。--local が --remote に変わっています。
npx wrangler d1 execute book-notes-db --remote --file=./schema.sql
最後に公開します。
npx wrangler deploy
https://book-notes.<あなたのサブドメイン>.workers.dev のようなURLが表示されれば完了です。このURLは、あなたのパソコンを閉じても動き続けます。友人に送ってみてください。
手元と本番は、まったく別の場所です
いま出てきた --local と --remote の違いは、最初につまずく箇所そのものです。整理しておきます。
図6:同じ wrangler のコマンドでも、効く先が2つに分かれます。手元のデータは本番へは届きません。
| コマンド | 効く場所 |
|---|---|
npx wrangler dev |
手元のパソコン |
wrangler d1 execute --local |
手元のデータベース |
wrangler d1 execute --remote |
本番のデータベース |
npx wrangler deploy |
本番のアプリ |
「手元では動くのに、公開したら動かない」というときは、ほぼ --remote の実行漏れです。
生成AIに書かせるときのコツ
ここからは、生成AIと組み合わせて使う場合に効く話です。
最初のプロンプトで、3つを指定する
やり直しを減らすには、最初に前提を渡しておくことが大切です。次の3つを書いておくと、出てくるコードの手直しが減るかと思います。
- Cloudflare Workers向けであること(「Node.jsのExpressで」と書かない)
-
データベースはD1で、bindingの名前は
DB(名前を先に決めてしまう) -
画面は
public/に置く静的ファイル、APIは/api/...(置き場所を指定する)
例えば、こう頼みます。
Cloudflare Workers上で動く読書メモアプリを作ってください。
- 画面は public/index.html に置く静的ファイル
- APIは /api/notes(GETで一覧、POSTで1件保存)
- データベースはD1、bindingの名前は DB
- SQLは必ず ? と bind() を使う
- wrangler.jsonc と schema.sql も一緒に出力する
生成AIが書きがちで、Workersでは動かない3つの形
Workersは、一般的なNode.jsのサーバーとは実行環境が違います。そのため、AIが学習してきた「普通のサーバー向けの書き方」がそのままでは動かないことがあります。よく出るのは次の3つです。
図7:この3つを直せば、生成AIの出力はだいたいそのまま動きます。
| 生成AIが書きがちな形 | Workersで動く形 | 理由 |
|---|---|---|
fs.readFileSync("data.json") |
env.DB.prepare(...) で読む |
Workersにはファイルシステムがありません |
new Database("app.db") |
env.DB(D1のbinding) |
better-sqlite3 などのnpm製データベースは動きません |
process.env.API_KEY |
fetch(request, env) の env.API_KEY
|
環境変数は起動時ではなく、リクエストごとに env で渡されます |
もし生成AIの出力にこれらが入っていたら、その部分だけ「Workersで動く形に直して」と伝えれば直ります。上の表をそのまま貼り付けて渡すのが手っ取り早いです。
公式のスキルを入れておく
前述のとおり、Claude Codeなら次の2行でCloudflare公式のスキル一式が入ります。
/plugin marketplace add cloudflare/skills
/plugin install cloudflare@cloudflare
CursorやCodexを使っている場合も、CloudflareのDocs for agentsにツール別の設定手順が用意されています。上の3つの間違いを、そもそもAIが出さなくなります。
APIキーをコードの外に置く
生成AIで作るアプリは、OpenAIやAnthropicのAPIを呼ぶことがよくあります。そのときのAPIキーの置き場所には、決まったやり方があります。
図8:置き場所は手元と本番で分かれますが、コードからの読み方は同じです。
まず、コードに直接書かないでください。GitHubに上げた瞬間に他人が読める状態になります。公開リポジトリのAPIキーは自動収集の対象になっていて、数分で使われることがあります。
手元での開発中は、プロジェクト直下に .dev.vars というファイルを作って書きます。
OPENAI_API_KEY=sk-xxxxxxxxxxxx
このファイルは必ず .gitignore に入れてください。
.dev.vars
本番側へは、コマンドで登録します。実行すると値の入力を求められます。
npx wrangler secret put OPENAI_API_KEY
一度登録すると、値は画面にもコマンドにも表示されなくなります。読み出す方法はコードからだけです。
コード側は、手元でも本番でも同じ書き方で済みます。
export interface Env {
DB: D1Database;
// secret も、型の上ではただの文字列として足すだけです
OPENAI_API_KEY: string;
}
export default {
async fetch(request, env): Promise<Response> {
// 手元では .dev.vars、本番では secret の値がここに入ります
const key = env.OPENAI_API_KEY;
const res = await fetch("https://api.openai.com/v1/models", {
headers: { Authorization: `Bearer ${key}` },
});
return Response.json({ ok: res.ok });
},
} satisfies ExportedHandler<Env>;
置き場所が変わってもコードは書き換え不要、というのがこの仕組みの利点です。
公開したあとにやること
GitHubにつないで、自動で更新する
毎回 wrangler deploy を打つのが面倒になったら、GitHubリポジトリをつないでおくと、pushするだけで公開が更新されるようになります。Workers Buildsという機能です。
設定はダッシュボードから行います。Workers & Pages で対象のWorkerを開き、Settings > Builds から Connect を選んでGitHubを認証します。
無料プランでも月3,000分のビルド時間が付いています。小さなアプリなら1回のビルドは1分程度なので、実質的に上限を気にせず使えます。
ダッシュボード上のWorker名と、wrangler.jsonc の name が一致していないとビルドが失敗します。両方を同じ名前にしてください。
独自ドメインを割り当てる
workers.dev のURLのままでも動きますが、独自ドメインを持っている場合はそちらを割り当てられます。SSL証明書(鍵マーク)は無料で自動的に用意されるので、証明書の取得や更新の作業はありません。
料金:どこから有料になるのか
無料プランのまま置いておける範囲と、有料に切り替わる境目を整理します。
| 項目 | 無料プラン | 有料プラン(月5ドルから) |
|---|---|---|
| Workersのリクエスト | 1日10万回 | 月1,000万回まで込み、超過分は100万回あたり0.30ドル |
| 1回あたりのCPU時間 | 10ミリ秒 | 既定30秒、設定で最大5分まで |
| D1の読み取り | 1日500万行 | 月250億行まで込み、超過分は100万行あたり0.001ドル |
| D1の書き込み | 1日10万行 | 月5,000万行まで込み、超過分は100万行あたり1ドル |
| D1の保存容量 | 合計5GB | 5GBまで込み、超過分は1GBあたり月0.75ドル |
| 静的アセットの配信 | 無料・無制限 | 無料・無制限 |
有料プランには月5ドルの最低料金があります。無料プランには最低料金がありません。
2026年9月からの変更点
2026年9月1日から、無料プランでD1の1日あたりの上限を超えると、その日はクエリがエラーを返すようになりました。保存済みのデータが消えるわけではなく、翌日の午前0時(UTC)に枠が戻ります。上限に達したときにはメールで通知が届きます。
請求が伸びる原因は、たいてい索引です
有料プランに切り替えたあとに気をつけたいのが、D1の課金が「読み取った行数」で数えられる点です。
索引(インデックス)のない列で絞り込むと、返ってくるのが1行でも、内部では表の全行を読み込みます。5,000行の表なら、1回の検索で5,000行分を読んだことになります。手順5の schema.sql で CREATE INDEX を書いたのは、これを抑えるためです。
なお、Cloudflareではデータの転送量(egress)に対する課金がありません。画像やファイルを多く配信するアプリでは、この差が効いてきます。
動かないときに確認すること
公開したのに動かない、というのは誰でも通る道です。原因を推測する前に、上から順に確認してください。
図9:上から順に切り分けると、多くはこの5つの範囲で見つかります。
| 症状・エラー | よくある原因 | 対処 |
|---|---|---|
| 画面は出るがデータが出ない | URLの末尾のパスを付け忘れている |
/api/notes まで含めて開く |
no such table と出る |
本番のデータベースに表を作っていない |
--remote を付けて schema.sql を実行する |
env.DB が undefined になる |
設定ファイルとコードで名前が違う |
binding の名前とコード上の名前をそろえる |
| データが空で返る | 手元にだけデータを入れている | 本番にもデータを入れるか、画面から登録する |
| APIキーが空になる | 本番に secret を登録していない |
npx wrangler secret put <名前> を実行する |
| 日次の上限に関するエラー | 無料枠を使い切った | 翌0時(UTC)を待つか、有料プランへ切り替える |
ここまでで見つからなければ、本番で動いているコードのログをその場で見られます。
npx wrangler tail
このコマンドを実行したまま、別のタブでアプリを開いてください。エラーが起きていれば、その内容がターミナルに流れてきます。
まとめ
要点を3つにまとめます。
-
公開に必要な作業は、実質2コマンドです。 ひな型を作る
npm create cloudflare@latestと、公開するnpx wrangler deploy。土台の管理はCloudflare側にあります - 画面・処理・データが1つのWorkerに収まります。 生成AIに1つのフォルダで作らせた構成が、そのまま公開できる形になっています
- 画面の配信は無料・無制限、数えられるのはコードが動いた回数だけです。 1日10万回まで無料で、カード登録も要りません
次の順番で進めるのがおすすめです。
- 手順1と2だけをやって、中身を何も変えずに
npx wrangler deployする。URLが返ってくることを確かめる - 手順3で画面を差し替えて、もう一度
deployする - 手順4と5でデータの保存を足す
いきなり完成形を目指さず、「公開できた」「画面が出た」「保存できた」を1つずつ確かめていくと、途中で詰まったときに原因の場所がすぐ分かります。生成AIに続きを頼むときも、どこまで動いているかを伝えられるので、話が早くなります。
この記事のコマンドと設定は、2026年9月時点の公式ドキュメントに沿って読書メモアプリ向けに書き換えたものです。通しで実行した記録ではないため、ご自身の環境で1つずつ確認しながら進めてください。料金や無料枠は変わることがあります。








