メインコンテンツまでスキップ

デプロイアーキテクチャ

システム構成

ブランチ戦略

GitHub Flow を採用。main ブランチが唯一のトランク。

API は常設の Staging 環境(subseek-api-staging)を持ち、PR ごとに最新のコードがデプロイされる。 Web は Vercel Preview で PR ごとに自動デプロイされる。staging ブランチは持たない。

なぜ常設 Staging 環境を持つのか

  • Better Auth ダッシュボード: プロジェクト接続に固定の Base URL が必要。動的 PR Preview URL では接続できない
  • 環境変数の安定性: wrangler secret で一度設定すれば PR ごとの再設定が不要
  • ブランチ戦略は GitHub Flow のまま: staging ブランチは導入せず、PR 作成時に常設 Staging にデプロイする
  • 1人開発: 複数 PR が同時にある場合は最後にデプロイされた PR の内容が Staging に反映される

アプリ名・URL 一覧

Production

コンポーネントアプリ名URL状態
Vercel (Web)subseekhttps://subseek.ccデプロイ済み
Cloudflare Workers (API)subseek-apihttps://subseek-api.workers.devデプロイ済み
Meilisearch (Hetzner US)Hetzner VM(Production ポート)デプロイ済み
Cloudflare R2subseek-backups未作成(v0.3 #77)
Turso (DB)subseeklibsql://subseek-saneatsu.aws-us-east-1.turso.io作成済み

Staging

常設の Staging 環境。PR 作成時に API が自動デプロイされる。Web は Vercel Preview で PR ごとに自動デプロイ。

コンポーネントアプリ名URL状態
Vercel (Web)subseek-<hash>-nito.vercel.app(PR ごとに動的)Vercel Preview デプロイ(GitHub 連携で自動)
Cloudflare Workers (API)subseek-api-staginghttps://subseek-api-staging.workers.devデプロイ済み
Meilisearch (Hetzner US)Hetzner VM(Staging ポート)(Production と同一 VM、別ポート)デプロイ済み
Turso (DB)subseek-previewlibsql://subseek-preview-saneatsu.aws-us-east-1.turso.io作成済み

デプロイ済みサービスの詳細

Meilisearch(Hetzner US セルフホスト)

Hetzner US(Ashburn)の VPS で Docker セルフホスト。詳細は 検索インフラ調査 および Meilisearch ホスティング比較 を参照。

  • Docker コンテナで起動(getmeili/meilisearch:v1.13
  • Production(:7700)/ Staging(:7701)を同一 VM 上で別コンテナとして運用
  • VM: Hetzner CPX22(Shared, Regular Performance)、IPv4: <HETZNER_VM_IP>
  • セットアップ手順: Meilisearch セットアップ
  • BAN リスク対策: Meilisearch ダンプを毎日 R2 に自動保存(v0.3 #77)

注意: infra/meilisearch/fly.toml は旧 Fly.io セルフホスト時の設定ファイル。現在は使用しないが、参考として残している。

Hono API(Cloudflare Workers)

環境URLヘルスチェック
Productionhttps://subseek-api.workers.dev/{"message":"subseek API"}
Staginghttps://subseek-api-staging.workers.dev/{"message":"subseek API"}
  • ランタイム: Cloudflare Workers(V8 isolate)
  • 互換性フラグ: nodejs_compat, nodejs_compat_populate_process_env
  • 設定ファイル: apps/api/wrangler.toml

Next.js Web(Vercel)

環境URL状態
Productionhttps://subseek.cc200 OK
  • フレームワーク: Next.js(自動検出)
  • Root Directory: apps/web
  • GitHub 連携: nito-tech/subseek リポジトリ

ドメイン

  • ドメイン: subseek.cc(取得済み)
  • DNS 管理: Cloudflare DNS で設定
  • Fly.io はデフォルトドメイン(*.fly.dev)を使用

なぜ API は api.subseek.cc ではなく subseek-api.workers.dev なのか

API の URL はフロントエンドが内部的にリクエストするだけで、ユーザーに直接見えることはない。そのため、カスタムドメインを設定するメリット(ブランド統一)よりも、設定の手間を省く方が合理的と判断した。

必要になった時点で api.subseek.cc を Workers カスタムドメインとして追加することは可能。NEXT_PUBLIC_API_URL 環境変数を変更するだけで対応できる。

CI/CD パイプライン

GitHub Flow を採用。PR 1 本につき 1 つの workflow(ci.yml)で CI → Staging Deploy → E2E を直列に実行する。CI が失敗した PR では Vercel build / Workers / Hetzner デプロイが走らないため、無駄なリソース消費を防ぐ。

paths-filter の監視対象

detect ジョブは dorny/paths-filter で PR の差分を検査し、対応するアプリの deploy ジョブを起動する。

アプリトリガーパス
API (deploy-api)apps/api/**, packages/**, pnpm-lock.yaml, .github/workflows/**
Web (deploy-web)apps/web/**, packages/**, pnpm-lock.yaml, .github/workflows/**
Meilisearch (deploy-meilisearch)infra/meilisearch/**, .github/workflows/**
DB マイグレーションpackages/db/migrations/**(→ deploy-api 内で drizzle-kit migrate を実行)

各トリガーパスの中身は次の通り:

パス内容
apps/api/**Hono API(Cloudflare Workers)のソースコード一式
apps/web/**Next.js Web のソースコード一式
packages/**モノレポ共通パッケージ(@subseek/db の Drizzle スキーマ、@subseek/shared の型・ユーティリティ等)。API と Web の両方が依存しているため両方を再デプロイ対象にしている
pnpm-lock.yamlpnpm のロックファイル。依存関係更新を検知して再デプロイする
infra/meilisearch/**Meilisearch のインフラ設定(Docker / 旧 fly.toml 等)
.github/workflows/**CI/デプロイ workflow 自体。これも監視対象に含めることで、workflow を編集した PR を staging で動作確認できる
packages/db/migrations/**Drizzle ORM のマイグレーション SQL。検知すると deploy-api ジョブの先頭で drizzle-kit migrate を staging DB に対して実行する

ワークフロー一覧

ファイルトリガー役割
ci.ymlPR 作成/更新lint/typecheck/vitest/knip → CI 全 success 後に paths-filter → staging deploy → E2E までを統合実行
ci-reusable.ymlworkflow_calllint/typecheck/vitest/knip の 4 並列ジョブ
e2e-reusable.ymlworkflow_callPlaywright E2E(base-url input を受け取る)
deploy-production.ymlmain への push(PR マージ)paths-filter → API(Workers)/Web(Vercel)/Meilisearch 本番デプロイ

運用ルール

  • CI ゲート: staging deploy は CI(lint/typecheck/vitest/knip)が全 success のときのみ走る。1 つでも失敗すると detect / deploy / e2e はすべて skip される
  • Draft PR: CI も staging deploy も全 skip。レビュー前の WIP を実インフラに流さないため
  • Web(Vercel): PR ready_for_review 後に Vercel CLI でデプロイし、固定エイリアス staging.subseek.cc に上書き割り当て。main マージで Production (subseek.cc) にデプロイ
  • API(Cloudflare Workers): PR ready_for_review 後に wrangler deploy --env staging で常設 Staging にデプロイ、main マージで wrangler deploy で Production デプロイ
  • Meilisearch: Hetzner US で Docker セルフホスト。通常デプロイ不要(設定変更・バージョンアップ時のみ手動)

技術選定の背景

なぜ Vercel(Web)なのか

  • Next.js の開発元で互換性 100%、ゼロコンフィグデプロイ
  • next-intl(7言語対応)の動作が保証されている
  • PR 連動の Preview デプロイが標準機能
  • GitHub 連携で CI/CD 設定が不要

なぜ Cloudflare Workers(API)なのか

  • Hono がネイティブサポートするランタイムで、export default app だけでデプロイ可能
  • V8 isolate でメモリ管理・スケーリングが自動(VM の OOM やコールドスタート問題がない)
  • $5/月の Paid プランで 10M リクエスト含む。50 万 PV 以上で Fly.io よりコスト優位
  • Docker ビルド不要で CI が高速
  • Fly.io を使用 → 256MB VM で OOM クラッシュが発生し Workers に移行(2026-04)

なぜ Hetzner US(Meilisearch)なのか

  • Meilisearch Cloud と比べて2〜7倍安い(10万動画で月3,000円 vs 25,500円)
  • 機能差・レイテンシ差なし(同じ US East、同じ Meilisearch 本体)
  • Docker 1コマンドで起動でき、運用は月0.5〜2時間
  • BAN リスクは R2 ダンプバックアップ + Meilisearch Cloud 即移行で対策
  • 詳細は Meilisearch ホスティング比較 を参照

なぜプライマリリージョンは US East(ewr)なのか

subseek の対応言語圏(日本語・英語・中国語・韓国語・スペイン語・フランス語・ドイツ語)の YouTube 利用者数を国別に集計し、各リージョンへの RTT で加重平均を算出した結果、US East(ewr)が最適と判断した。

プライマリカバー人口(RTT < 100ms)加重平均書き込みレイテンシ
東京(nrt)1.72億(日本・韓国・台湾・香港)~155ms
US East(ewr)5.61億(米・加・墨・英・仏・独・西)~72ms

iad は nrt の 3.3倍の人口 を低レイテンシでカバーできる。

設計判断

  • API・DB・Meilisearch はすべて同一リージョン(iad)に配置する: API↔DB 間は 1 リクエストで複数回アクセスするため、ここのレイテンシが最もクリティカル
  • Turso のプライマリロケーションは作成後に変更できない(不変): read replica をプライマリに昇格させる機能も存在しないため、データが少ないうちに最適なリージョンで作成した
  • 将来のマルチリージョン化: ユーザー増加時に東京(nrt)に Fly.io マシン + Turso read replica を追加し、日本・韓国ユーザーのレイテンシを改善する方針
  • Meilisearch(Hetzner US Ashburn): Fly.io API(ewr)と同じ US East。RTT 5〜20ms

なぜ iad(Virginia)ではなく ewr(New Jersey)なのか

Fly.io の iad リージョンでは 2025年2月後半からボリューム付きマシン作成時に insufficient CPUs エラーが継続しており(Fly.io Community)、Meilisearch のデプロイが不可能だった。ewr は同じ米国東海岸に位置し、Turso の US East(Virginia)までの RTT は ~3-5ms と十分近いため、代替リージョンとして採用した。

なぜ Cloudflare Workers ではないのか

  • Meilisearch のセルフホストが不可能(Docker 非対応)
  • API を Workers に置くと Meilisearch(Fly.io)との間にネットワークホップが増加
  • 収益に対するサーバー代比率が 1% 以下と微小なため、コスト差よりも運用のシンプルさを優先

疎通確認結果(2026-04-09)

経路結果備考
Web → API(CORS)OKaccess-control-allow-origin: https://subseek.cc
API → Turso DBOKProduction / Staging 両方
API → Meilisearch(Hetzner)OKヘルスチェック OK(:7700, :7701)
Google OAuth要調査/api/auth/sign-in/social が 500 エラー
API → R2N/Av0.3(#77)で導入予定
Stripe 決済N/AAPI 側ハンドラー未実装(#145)