デプロイと観測性 — Vercel・Cloudflare Pages・Sentry・PostHog
Next.js を Vercel と Cloudflare Pages にデプロイ、preview deploy、環境変数、Sentry でエラー追跡、PostHog でプロダクト分析。リリース後4週間のための道具一式。
32章で認証とセッションを扱いました。本章は5部(運用・テスト・デプロイ)の最終章として、出来上がったアプリを実運用環境に上げ、それ以降を運用する道具を見ていきます。
「デプロイ」までは多くの入門書が扱いますが、デプロイ以降 まで扱う資料は意外と少ないです。リリース最初の4週間が最も厳しい区間です。新しいユーザーが最初に出会ったエラーを私たちは知らず、どこで離脱しているのかも知りません。観測性(observability)の道具がその空白を埋めます。本章では Vercel と Cloudflare Pages へデプロイし、Sentry でエラーを追跡し、PostHog でプロダクト分析を始める1サイクルを見ていきます。
本章の道具一式は次の34章(フルスタック Todo キャプストーン)でそのまま使われます。そして31章(パフォーマンスと Web Vitals)の実運用 RUM データが、本章の PostHog と出会います。
ホスティング選択の出発点 #
Next.js を production に上げる最も普遍的な2つの選択肢が Vercel と Cloudflare Pages です。両者とも PR ごとに preview deploy を自動で作り、GitHub と統合され、1 commit 単位でロールバックが可能です。
選択の出発点は次の4つです。
- トラフィックの形 — 関数呼び出しの多いアプリか、静的の比重が大きいアプリか
- 付帯サービス依存 — KV / D1 / Workers のような Cloudflare サービスに紐づく価値があるか
- コスト曲線 — 無料区間が十分か、有料に移ったときの単価
- 運用の利便性 — Vercel は Next.js の本家、Cloudflare はグローバルエッジ
小さなサイドプロジェクトの一般的な選択は Vercel で開始 → トラフィックが増えたらコストを再評価 です。本章もその流れを追います。
Vercel 1 サイクル #
最初のデプロイ #
1. GitHub に repo を push
2. vercel.com → New Project → repo を選択
3. Framework: Next.js (自動認識)
4. Environment Variables: AUTH_SECRET, AUTH_GITHUB_ID, AUTH_GITHUB_SECRET など
5. Deploy をクリック設定はほぼなしで 1〜2分以内に production URL が生まれます。同じ repo に push が入るたびに新しい deploy が自動的に作られ、PR ごとに別の preview URL が生まれます。
Preview Deploy の価値 #
PR ごとに新しい URL が生まれることは単なる便利機能ではありません。30章の Playwright を preview URL で回せば、production ビルド固有のバグを捕まえます。
name: preview-e2e
on:
pull_request:
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- run: pnpm install --frozen-lockfile
- run: pnpm exec playwright install --with-deps
- name: Wait for Vercel preview
uses: patrickedqvist/wait-for-vercel-preview@v1
id: preview
with:
token: ${{ secrets.GITHUB_TOKEN }}
max_timeout: 180
- run: pnpm exec playwright test
env:
PREVIEW_URL: ${{ steps.preview.outputs.url }}PR を開くと Vercel が preview を作り、GitHub Actions がその URL が生きるまで待ってから Playwright を回します。production ビルド + 実データベースで回る E2E なので、dev サーバーでは見えなかったバグが捕まります。
環境変数 — 環境別に分離 #
Vercel は環境変数を3つの環境に分離します。
- Production:
mainブランチの deploy - Preview: すべての PR / その他ブランチの deploy
- Development: ローカル(
vercel env pullで同期可能)
.env.local (gitignore 対象、ローカル開発のみ)
↑
Vercel CLI: `vercel env pull` で development 環境変数を .env.local に取得
↓
Vercel Web: Production / Preview / Development の各環境ごとに入力AUTH_SECRET のようなシークレットは production と preview で互いに違っているべきです(preview の事故が production に影響しないように)。ただし OAuth callback URL は preview URL が動的に変わるため、OAuth App 設定で wildcard を使うか、preview 用 App を別に持つのが一般的です。
料金と落とし穴 #
Vercel の無料(Hobby)プランは学習 / 個人プロジェクトに十分です。ただし production を上げようとする時点では次を確認します。
- Function Invocations: Server Action / API ルートの呼び出し数。無料は月 100k invocations。
- 画像変換(
next/image): 無料は月 5,000枚。トラフィックの多いサイトは早々に上限にぶつかります。 - bandwidth: 無料 100GB / 月。
- 商業利用: Hobby プランは非商業限定。収益が発生したら Pro プランへ移る必要があります。
next/image の画像変換上限 が意外な落とし穴です。CDN キャッシュが切れた時点で変換が再度起きると上限を急速に消費します。トラフィックが多くなりそうなサイトは、自前の画像ホスティング(Cloudflare R2 / Images、S3 + CloudFront)を併用するのが安全です。
Cloudflare Pages 1 サイクル #
Next.js を Cloudflare で動かす #
Cloudflare Pages は静的サイトが強みで、Workers ベースの動的ページもサポートします。Next.js の RSC + Server Action を Cloudflare で動かすには @cloudflare/next-on-pages アダプタを使います。
pnpm add -D @cloudflare/next-on-pages{
"scripts": {
"build:cf": "next build && npx @cloudflare/next-on-pages",
"preview:cf": "wrangler pages dev .vercel/output/static"
}
}next.config.ts で一部のオプションを Cloudflare 互換に設定する必要があります(詳細は公式ドキュメントを参照)。
Workers / KV / D1 との結合 #
Cloudflare の価値は 付帯サービスとの自然な結合 にあります。
- KV — キーバリューストア。セッションやキャッシュのような単純データ。
- D1 — SQLite ベースのサーバーレス DB。価格が魅力。
- R2 — S3 互換のオブジェクトストレージ。egress 費用が無料。
- Images — 画像変換 + CDN。
next/imageの上限を大きく引き上げます。
これらの付帯サービスを同じ Workers ランタイム内で呼び出すと、latency が極めて低くなります。グローバルエッジに分散されたアプリ を作ろうとするなら Cloudflare に分があります。
料金比較 #
| 項目 | Vercel Hobby(無料) | Cloudflare Pages 無料 |
|---|---|---|
| Bandwidth | 100GB/月 | 無制限 |
| Function Invocations | 100k/月 | 100k リクエスト/日 |
| Build minutes | 6,000分/月 | 500分/月 |
| 付帯サービス | 別 | KV / D1 / R2 統合 |
数値はポリシー変更により頻繁に変わるので、リリース時点で最新情報を再確認するのが安全です。
落とし穴 — ツール別ファイル増殖 #
Cloudflare Pages は 1 deploy あたりのファイル数制限があります(時点基準で 20,000個)。Hugo / Next.js static export のような静的サイト生成器はカテゴリ / タグ別にページを掛け算式に作ることがあり、記事が 200個でも実ファイルが数万個になることがあります。事前点検なしに無料上限を仮定するとリリース直前で詰まります。
これは schoolofweb.net 運営中に直接ぶつかった落とし穴でもあります。静的サイトツール + カテゴリ / タグ / シリーズの multiplier がある環境では、ビルド後のファイル数を先に数えてからホスティングを決めます。
ホスティング選択の決定木 #
Next.js フルスタックアプリか?
├── Yes
│ ├── グローバル edge が必要か? (全世界のユーザー)
│ │ ├── Yes → Cloudflare Pages + Workers
│ │ └── No
│ │ ├── Vercel の制限内で運用可能か?
│ │ │ ├── Yes → Vercel
│ │ │ └── No → Cloudflare Pages またはセルフホスティング
└── No (静的サイト)
└── Cloudflare Pages または Netlify または GitHub Pages最初は Vercel、トラフィック / コストが問題になったら Cloudflare へ移行 が最も一般的な流れです。両ホスティングとも Next.js を標準入力として受けるので、移行コストは大きくありません(環境変数 / OAuth callback URL くらい)。
環境変数とシークレット管理 #
ビルド時 vs 実行時 #
Next.js の環境変数は2つの時点で評価されます。
- ビルド時:
NEXT_PUBLIC_プレフィックスのついた変数。ビルド時点でコードにインライン化されます。 - 実行時: それ以外の変数。サーバーコード実行時に
process.envでアクセス。
// ビルド時 (クライアントに公開される)
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
// 実行時 (サーバーのみ、クライアントバンドルに含まれない)
const dbUrl = process.env.DATABASE_URL;
const authSecret = process.env.AUTH_SECRET;NEXT_PUBLIC_ プレフィックスは「この値はクライアントバンドルに入っても安全」 という意図の宣言 です。シークレットには絶対につけないでください。
シークレット露出の検証 #
production ビルド後に .next/static/chunks/ のファイルを grep し、シークレットが含まれていないか一度確認する習慣が安全です。
pnpm build
grep -r "AUTH_SECRET\|<実際のシークレットの一部>" .next/staticマッチしてはいけません。32章の AUTH_SECRET が production バンドルに紛れ込んだなら、OAuth provider のシークレットを即座に再発行するのが定石です。
Sentry — エラー追跡 #
production で発生したエラーを自動で収集し1箇所で見る道具です。発生位置、スタックトレース、ユーザー情報、直前の動作(breadcrumb)まで一緒に残します。
インストールとセットアップ #
pnpm add @sentry/nextjs
pnpm exec @sentry/wizard@latest -i nextjswizard が自動的に sentry.client.config.ts / sentry.server.config.ts / sentry.edge.config.ts の3ファイルを作り、next.config.ts をラップします。
自動で追跡されるもの #
インストールするだけで次が自動で追跡されます。
- 未処理の JavaScript 例外(client / server 両方)
- 未処理の Promise rejection
- Server Action / API Route のエラー
- React error boundary が捕まえたエラー
import * as Sentry from '@sentry/nextjs';
try {
await riskyOperation();
} catch (err) {
Sentry.captureException(err, { tags: { feature: 'payment' } });
throw err;
}タグとコンテキストを併せて送ると、ダッシュボードでのフィルタリングが楽になります。
Source map のアップロード #
production ビルドは minified なのでスタックトレースは (anonymous):1:23456 のような形になります。source map を Sentry にアップロードすると、元のコード位置に再マッピングされます。wizard がこの部分も自動設定します。
シークレット情報の自動マスキング #
デフォルトで password、token、secret のようなフィールド名は自動でマスキングされます。ただし自前のフィールド名がある場合は明示的に追加します。
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
beforeSend(event) {
if (event.request?.data?.creditCard) {
delete event.request.data.creditCard;
}
return event;
},
});beforeSend で送る前にもう一度加工できる段階があります。
通知と優先度 #
Sentry ダッシュボードで、同じエラー(fingerprint 基準)が N 回発生したら Slack / メールで通知が飛ぶよう設定します。すべてのエラーに即時通知はノイズ です。次あたりが普通の出発点です。
- 新しいエラーが production で初発生 → 即時通知
- 同じエラーが時間あたり100件超え → 即時通知
- その他 → 日次サマリ
PostHog — プロダクト分析 #
Sentry が「何が間違ったか」を見るなら、PostHog は「ユーザーが何をしているか」を見ます。離脱地点、funnel 転換率、機能の利用頻度のようなデータを集めます。
インストール #
pnpm add posthog-js posthog-node'use client';
import posthog from 'posthog-js';
import { PostHogProvider } from 'posthog-js/react';
import { useEffect } from 'react';
if (typeof window !== 'undefined') {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY!, {
api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
person_profiles: 'identified_only',
});
}
export function Providers({ children }: { children: React.ReactNode }) {
return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
}Autocapture と明示的イベントのバランス #
PostHog の強みの一つが autocapture — すべてのクリックとページビューを自動で収集します。セットアップしただけで即座に funnel 分析が可能になります。
ただし autocapture が万能なわけではありません。ユーザーのインテントがある行動(登録完了、決済完了、中核機能の利用)は 明示的イベント として別に取るほうが信頼性が高いです。
'use client';
import posthog from 'posthog-js';
function CheckoutButton() {
function handleClick() {
posthog.capture('checkout_completed', {
amount: 9900,
currency: 'KRW',
plan: 'pro',
});
// ... 決済処理
}
return <button onClick={handleClick}>決済</button>;
}31章の Web Vitals との結合 #
31章で useReportWebVitals を使って収集した Web Vitals データを PostHog に送ると、実ユーザーの LCP / INP / CLS の分布 をダッシュボードで見られます。
'use client';
import { useReportWebVitals } from 'next/web-vitals';
import posthog from 'posthog-js';
export function WebVitals() {
useReportWebVitals(metric => {
posthog.capture('web_vital', {
name: metric.name,
value: metric.value,
rating: metric.rating,
});
});
return null;
}PostHog の insights 画面で LCP の p75 / p95 を時系列で見ると、31章で見た lab データとは違う実ユーザー分布が現れます。
Feature Flag #
PostHog の付帯機能として feature flag があります。新機能を一部ユーザーにだけ露出したり A/B テストを行うときに使います。
'use client';
import { useFeatureFlagEnabled } from 'posthog-js/react';
function PricingPage() {
const newPricing = useFeatureFlagEnabled('new-pricing-page');
return newPricing ? <NewPricing /> : <OldPricing />;
}flag は PostHog ダッシュボードでユーザーグループごとに on/off します。コードのデプロイなしで露出比率を調整できます。
CI 統合 — 5部全体を1つの流れに #
29〜33章のすべての道具を束ねた CI の流れを一度描いてみます。
1. lint + type check (即時)
2. Vitest 単体 / 統合テスト (29章)
3. Next.js build (production ビルド)
4. Vercel preview deploy (自動)
5. Playwright E2E (preview URL で、30章)
6. Lighthouse CI (preview URL で、31章)
7. PR レビュー + マージ
8. main へマージ → production deploy
9. Sentry / PostHog に source map を自動アップロード各段階が通過してこそ次に進みます。マージされたコードはすでに production ビルドで一度検証された状態 です。
この流れがリリース後の4週間を恐れずに済む土台になります。新しいコードが production に届く前に自動安全網を通過し、届いた後は Sentry / PostHog が何が起きているかを教えてくれます。
リリース後最初の4週間のチェックリスト #
5部全体の締めくくりとして、実際にリリース後最初の4週間に頻繁にぶつかる項目を整理しておきます。
- 最初の24時間: Sentry の通知が正常に動くか、新しいエラーが積み上がりすぎていないかをモニタリング。autocapture だけで funnel が描けるか PostHog で確認。
- 最初の週: Web Vitals の実ユーザー分布を確認(lab と違うことがあります)。LCP / INP の p75 が「Good」範囲にありますか。
- 2週目: 最もよくあるエラー3つを選んで優先処理。新しいエラーが発生しないよう retry / fallback / 明確なメッセージを追加。
- 3週目: ユーザー funnel 分析。最も多く離脱する場所を PostHog で識別。その段階の UX またはパフォーマンス改善。
- 4週目: 上限 / コスト点検。Vercel function invocations、画像変換、bandwidth が無料上限内に収まっているか。必要に応じてホスティング / 画像ホスティングを再評価。
自分でやってみよう — フルサイクルを1回回す #
本書の例題アプリまたは自分の小品を選び、次を一度最後まで回してみてください。
- Vercel デプロイ: GitHub repo → Vercel import → 環境変数を入力 → production deploy。
- Preview deploy の確認: 新しいブランチを作り、1行修正して PR を開きます。preview URL が自動で作られることを確認。
- Playwright on preview: 先ほどの
preview-e2e.ymlを追加し、PR をもう一度 push して CI が preview URL で E2E を回すことを確認。 - Sentry セットアップ: wizard でインストール後、意図的にエラーを投げるページ(
throw new Error('test'))を作ってみてください。production にデプロイしてアクセスすると、Sentry ダッシュボードに source map で位置にマッピングされたスタックトレースが到着します。 - PostHog セットアップ: 先ほどの Providers を追加し、登録または中核アクション1つに明示的イベントを送ってみてください。PostHog ダッシュボードに funnel を一度描いてみます。
- Web Vitals → PostHog: 31章の
useReportWebVitalsを PostHog に繋げ、実ユーザー分布が流れ込んでくることを確認します。
6ステップを経ると、5部全体の道具が1つの流れに自然に繋がります。
練習問題 #
- Vercel vs Cloudflare の選択. 次の3つのアプリにどちらのホスティングが適しているかを答え、理由を書いてみてください。(a) 単一ドメインの小さな SaaS、トラフィックは月 100k ページビュー想定、韓国ユーザー中心、(b) 全世界ユーザーを狙う動画メタサイト、トラフィックが急増する可能性、(c) 静的マークダウンブログ、記事 5000本、カテゴリ / タグの multiplier。
- NEXT_PUBLIC_ vs 一般変数. 次の変数それぞれに
NEXT_PUBLIC_プレフィックスをつけるべきかを答えてください。(a) Stripe publishable key、(b) Stripe secret key、(c) PostHog project API key、(d) DATABASE_URL、(e) Sentry DSN。答えを書いてから本文のビルド時 vs 実行時の節を参考にします。 - エラー vs 分析の分担. 次の5つのデータを Sentry / PostHog のどちらの道具で扱うのが自然かを答えてください。(a) Server Action で発生した未処理エラー、(b) 登録完了イベント、(c) production の Web Vitals 分布、(d) 決済 funnel 転換率、(e) クライアントの JS 例外。両方とも可能な項目があれば、どちらに置くのがより適切か1行書きます。
一行まとめ: Next.js フルスタックは通常 Vercel で始め、トラフィック / コストに応じて Cloudflare Pages へ移行する流れが標準で、PR ごとに作られる preview deploy が production ビルド固有のバグを捕まえる中核の安全網です。
NEXT_PUBLIC_プレフィックスは「この値をクライアントに露出しても安全」 という意図の宣言で、シークレットには絶対つけません。Sentry は production のエラーを source map と一緒に1箇所に集め、PostHog は autocapture と明示的イベントで funnel / Web Vitals 分布を捕まえます。29〜33章の道具を1つの流れの CI に束ねておけば、リリース後最初の4週間が怖くなくなります。
次の章 #
本章で5部(運用・テスト・デプロイ)が完全に終わります。次の 34章 フルスタック Todo アプリを完成させる から6部(総合実習)が始まります。34章は 1〜33章で作ったすべての道具を1つの小さなフルスタックアプリに編み込むキャプストーンです。RSC + Server Actions + 認証 + DB 永続化 + テスト + デプロイ + 観測まで、本書全体の流れが1つの動くサービスに集まる地点です。