Astro×D1×Drizzle 副業アプリ0円構成ガイド

Astro×D1×Drizzle 副業アプリ0円構成ガイド 使い方・設定
Astro×D1×Drizzle 副業アプリ0円構成ガイドのイメージ

副業で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データベース

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 の設定: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_idwrangler.tomldatabase_id = "..." に貼り付けてください。複数サービスを運用する場合は、それぞれ別のデータベースを作成し、各アプリの wrangler.toml に設定します。

drizzle.config.ts とマイグレーションワークフロー

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にアクセスする方法

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.mtsplatformProxy: { 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の構成なら、その両方をクリアする手助けになります。

コメント