
副業でWebアプリを作ろうとしたとき、最初に立ちはだかるのがインフラコストの問題です。「月数百円でもランニングコストがかかると継続が億劫になる」という声はインディーハッカーのコミュニティでよく聞きます。Vercelの無料枠が縮小され、Supabaseも一定のトラフィックを超えると月数千円になるなど、「0円で動かせる構成」の選択肢が年々狭まっています。
この記事では、Astro 5 + Cloudflare Pages + Cloudflare D1 + Drizzle ORM という構成で、月額0円のフルスタックWebアプリを構築する方法について解説します。実際にこの構成で動かしているyarutoko・shugi-navi・kazoku-hikakuの3サービスのmonorepo運用から得た知見をもとに、ハマりやすいポイントも含めて紹介します。
結論から言うと、Cloudflare D1の無料枠は副業アプリの初期フェーズに十分すぎるほどの容量があり、0円でSQLiteベースのフルスタックアプリが運用できます。実際に3サービスを6ヶ月以上運用して無料枠を超えたことは一度もありません。
📋 この記事でわかること
- Cloudflare D1の無料枠と他サービスとのコスト比較
- Astro 5 + Cloudflare Pagesのセットアップ手順
- wrangler.toml と drizzle.config.ts の正しい設定方法
- ローカル開発から本番デプロイまでのマイグレーションワークフロー
- monorepoでの共通DBパッケージの構成パターン
- 実際の運用で遭遇したハマりどころと解決策
Cloudflare D1を選ぶ理由:0円で使えるSQLiteデータベース

副業アプリに使えるデータベースサービスは複数ありますが、2026年時点でのコスト比較を見ると、Cloudflare D1の無料枠が飛び抜けて充実しています。
主要データベースサービスの無料枠比較
| サービス | 無料ストレージ | 読み取り/日 | 書き込み/日 | 月額コスト(無料枠超過後) |
|---|---|---|---|---|
| Cloudflare D1 | 5GB | 25M行 | 100K行 | $0.001/100万行読み取り |
| Supabase | 500MB | 無制限 | 無制限 | $25/月〜 |
| Turso | 8GB(9DBまで) | 月500M行 | 月25M行 | $9/月〜 |
| Neon | 512MB | 月190時間 | 月190時間 | $19/月〜 |
| Railway PostgreSQL | 512MB | 無制限 | 無制限 | $5/月〜 |
Cloudflare D1の25M rows/dayという読み取り上限は、1秒あたり約289回のクエリに相当します。副業アプリが月間1万〜10万PVの段階では、まず超えることはありません。
ストレージも5GBあれば、テキスト中心のアプリなら数百万件のレコードが保存できます。例えば、タスク管理アプリで1レコードを1KBと仮定すると、500万件以上のデータが無料枠内に収まる計算になります。
Cloudflare Pages + D1 が副業向きな理由
Cloudflare Pagesは静的ファイルのホスティングが無制限で無料です。Astro 5は静的生成(SSG)とサーバーサイドレンダリング(SSR)を混在させることができるため、「基本は静的、DBアクセスが必要な箇所だけSSR」という理想的な構成が取れます。
さらに2025年にAstroがCloudflareグループに参加したことで、両者の統合がより密になり、開発体験が向上しています。GitHubと連携するだけで自動デプロイが設定でき、git push するだけで本番反映される点も副業開発に向いています。Cloudflare Workersのコールドスタート問題もAstroのSSGと組み合わせることで実質解消されており、「重い処理はビルド時に済ませ、動的な部分だけAPIエンドポイントで処理する」という設計が自然にできます。
セットアップ手順:必要なパッケージのインストールと設定

パッケージのインストール
# Astroプロジェクトの作成
pnpm create astro@latest my-app
cd my-app
# Cloudflareアダプターの追加
pnpm astro add cloudflare
# Drizzle ORMのインストール
pnpm add drizzle-orm
pnpm add -D drizzle-kit wrangler
astro.config.mts の設定
output: "server" と @astrojs/cloudflare アダプターを設定してください。
// astro.config.mts
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server',
adapter: cloudflare({
platformProxy: {
enabled: true,
},
}),
});
platformProxy を有効にすることが重要なポイントです。この設定を入れることで、astro dev コマンドでもローカルのD1バインディングにアクセスできるようになります。設定を忘れると「bindings is not defined」エラーが発生するので注意してください。
wrangler.toml の設定:D1バインディングの定義

wrangler.toml はCloudflare Workers/Pagesの設定ファイルです。D1データベースを使うには [[d1_databases]] セクションで定義する必要があります。
# wrangler.toml
name = "my-app"
compatibility_date = "2024-12-01"
compatibility_flags = ["nodejs_compat"]
[[d1_databases]]
binding = "DB"
database_name = "my-app-db"
database_id = "YOUR_DATABASE_ID_HERE"
binding の値が後ほどAstroコード内で使うバインディング名になります。慣習的に DB が使われていますが、任意の名前を付けられます。database_id はD1データベース作成後にCloudflareダッシュボードまたはwranglerコマンドで取得します。
D1データベースの作成方法
# D1データベースを作成
pnpm wrangler d1 create my-app-db
# 出力例:
# Created D1 database 'my-app-db'
# database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
出力された database_id を wrangler.toml の database_id = "..." に貼り付けてください。複数サービスを運用する場合は、それぞれ別のデータベースを作成し、各アプリの wrangler.toml に設定します。
drizzle.config.ts とマイグレーションワークフロー

drizzle.config.ts の設定で最も混乱しやすいのが driver の扱いです。シンプルな推奨フローを先に説明します。
推奨ワークフロー:
1. drizzle-kit generate でSQLファイルを生成する(drizzle.config.ts を使用)
2. wrangler d1 migrations apply でSQLを実行する(wrangler を使用)
drizzle.config.ts に driver: "d1-http" を設定するのは、Drizzle Studio等でリモートD1を直接ブラウズしたい場合のみです。日常のマイグレーション作業ではwrangler経由で実行するため、driver設定は省略できます。
// drizzle.config.ts(シンプル版:日常のマイグレーション用)
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'sqlite',
});
スキーマの定義
実際のyarutokoで使っているパターンを参考にしたスキーマ例を示します。
// src/db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const boards = sqliteTable('boards', {
id: text('id').primaryKey(),
title: text('title').notNull(),
description: text('description'),
created_at: integer('created_at', { mode: 'timestamp' }).notNull(),
updated_at: integer('updated_at', { mode: 'timestamp' }).notNull(),
});
export const tasks = sqliteTable('tasks', {
id: text('id').primaryKey(),
board_id: text('board_id').notNull().references(() => boards.id, { onDelete: 'cascade' }),
title: text('title').notNull(),
status: text('status').notNull().default('todo'),
position: integer('position').notNull().default(0),
created_at: integer('created_at', { mode: 'timestamp' }).notNull(),
updated_at: integer('updated_at', { mode: 'timestamp' }).notNull(),
});
export const participants = sqliteTable('participants', {
id: text('id').primaryKey(),
board_id: text('board_id').notNull().references(() => boards.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
email: text('email'),
joined_at: integer('joined_at', { mode: 'timestamp' }).notNull(),
});
カラム名は snake_case を使うのがD1の慣習です。TypeScript側のプロパティ名はスキーマ定義のキー名(左辺)がそのまま使われます。たとえば created_at: integer('created_at') と定義すれば record.created_at でアクセスできます。camelCase でアクセスしたい場合は createdAt: integer('created_at') のようにキー名だけ変えてください。
マイグレーションの実行手順
# ステップ1: Drizzle KitでSQLマイグレーションファイルを生成
pnpm drizzle-kit generate
# drizzle/ ディレクトリにSQLファイルが生成される
# 例: drizzle/0000_initial_schema.sql
# ステップ2: ローカル環境にマイグレーションを適用
pnpm wrangler d1 migrations apply DB --local
# ステップ3: 本番環境(リモート)にマイグレーションを適用
pnpm wrangler d1 migrations apply DB --remote
--local はローカルのD1エミュレーター(.wrangler/state/v3/d1/ に保存)に適用します。本番のD1には影響しないため、安全に開発できます。本番への適用は必ず --remote を付けて実行してください。スキーマ変更時はこの手順を繰り返します。
AstroのAPIエンドポイントでD1にアクセスする方法

バインディングの取得とDrizzleクライアントの初期化
Astro 5 + Cloudflareアダプターでは、context.locals.runtime.env 経由でD1バインディングにアクセスします。
// src/pages/api/tasks.ts
import type { APIContext } from 'astro';
import { drizzle } from 'drizzle-orm/d1';
import { tasks } from '../../db/schema';
export async function GET(context: APIContext) {
const db = drizzle(context.locals.runtime.env.DB);
const allTasks = await db.select().from(tasks).orderBy(tasks.position);
return new Response(JSON.stringify(allTasks), {
headers: { 'Content-Type': 'application/json' },
});
}
export async function POST(context: APIContext) {
const db = drizzle(context.locals.runtime.env.DB);
const body = await context.request.json();
const newTask = await db.insert(tasks).values({
id: crypto.randomUUID(),
board_id: body.boardId,
title: body.title,
status: 'todo',
position: 0,
created_at: new Date(),
updated_at: new Date(),
}).returning();
return new Response(JSON.stringify(newTask[0]), {
status: 201,
headers: { 'Content-Type': 'application/json' },
});
}
TypeScript型定義の追加
Cloudflareバインディングの型を定義しておくと、IDEの補完が効くようになります。この設定を入れることで、context.locals.runtime.env.DB にアクセスしたときに型エラーが出なくなります。
// src/env.d.ts
/// <reference types="astro/client" />
type D1Database = import('@cloudflare/workers-types').D1Database;
interface Env {
DB: D1Database;
}
type Runtime = import('@astrojs/cloudflare').Runtime<Env>;
declare namespace App {
interface Locals extends Runtime {}
}
monorepoでの共通DB構成:3サービス運用の実践パターン
yarutoko・shugi-navi・kazoku-hikakuの3サービスはpnpm workspacesのmonorepoで管理しています。各サービスは独立したCloudflare Pagesプロジェクトとしてデプロイされますが、Drizzleのクライアントファクトリ関数を共通パッケージとして切り出すことでコードの重複を避けています。
パッケージ構成
apps/
yarutoko/
wrangler.toml
astro.config.mts
shugi-navi/
wrangler.toml
astro.config.mts
kazoku-hikaku/
wrangler.toml
astro.config.mts
packages/
db/
src/
schema.ts
index.ts
drizzle/
drizzle.config.ts
package.json
共通DBパッケージのエントリポイント
// packages/db/src/index.ts
import { drizzle } from 'drizzle-orm/d1';
import * as schema from './schema';
export function createDb(d1: D1Database) {
return drizzle(d1, { schema });
}
export * from './schema';
各アプリからは createDb(context.locals.runtime.env.DB) と呼び出すだけで、型安全なDrizzleクライアントが取得できます。pnpm workspacesの設定では、各アプリの package.json に "@my-org/db": "workspace:*" と追記して参照します。
よくある質問
Q: astro dev でD1に接続できないのはなぜですか?
A: astro.config.mts の platformProxy: { enabled: true } を確認してください。この設定がない場合、通常の astro dev ではCloudflareバインディングにアクセスできません。
Q: ローカルのD1データはどこに保存されますか?
A: .wrangler/state/v3/d1/ ディレクトリにSQLiteファイルとして保存されます。このディレクトリは .gitignore に追加してください。
Q: D1の25M rows/dayを超えそうになったらどうすればいいですか?
A: まず SELECT * を避けて必要なカラムのみ取得する最適化を行いましょう。インデックスを追加することで読み取り行数を削減できます。それでも足りない場合はCloudflareのPaidプラン($5/月〜)に移行すると上限が大幅に上がります。
Q: デプロイはどのように行いますか?
A: Cloudflare PagesとGitHubリポジトリを連携しておけば、git push するだけで自動デプロイされます。デプロイ前に wrangler d1 migrations apply DB --remote でスキーマ変更を本番に適用しておくことをおすすめします。
Q: monorepoで pnpm wrangler d1 migrations apply を実行する場所は?
A: 各アプリのディレクトリ(wrangler.toml がある場所)か、packages/db/ ディレクトリで実行します。pnpm workspacesの場合、-F フラグを使って pnpm -F yarutoko wrangler d1 migrations apply DB --local のように実行するのがおすすめです。
✅ まとめ
この記事では、Astro 5 + Cloudflare Pages + D1 + Drizzle ORMを使って月額0円で副業アプリを構築する構成について解説しました。
- コスト: Cloudflare D1の無料枠(5GB・25M rows/day)は個人の副業アプリなら当面超えない
- 開発体験: Drizzle ORMで型安全なSQLが書ける、wranglerによるローカルエミュレーションが充実
- スケーラビリティ: 必要になれば$5/月〜でPaidプランに移行可能、スケールアップの障壁が低い
- monorepo対応: pnpm workspacesで複数サービスを共通DB構成で管理できる
次のアクションとして、まず pnpm wrangler d1 create your-app-db でD1データベースを作成して、この記事の手順を上から試してみてください。wrangler.toml・drizzle.config.ts・スキーマ定義の3ファイルを整えれば、最初のマイグレーションまで30分以内で到達できます。
副業アプリの最初の壁はコストではなく、動くものを早く作ることです。Cloudflare + Astroの構成なら、その両方をクリアする手助けになります。


コメント