ローカルでは完璧に動く。tsc も通る。テストも緑。なのに本番にデプロイしたら、なぜか壊れている——個人開発でいちばん心が折れるのが、この「ローカルで再現しない本番トラブル」です。
筆者は本業のかたわら、Next.js + Vercel で複数のサービス(Webアプリ・ブログ)を個人開発・運用しています。その過程で、ビルドも型チェックもユニットテストも全部通っているのに本番だけが壊れるというトラブルを何度も踏みました。しかもその多くが「エラーすら出ない(静かに壊れる)」タチの悪いものでした。
この記事は、その中でも特に「知っていれば数時間〜半日を溶かさずに済んだ」と痛感した4つを、症状 → 原因 → なぜローカルで気づけなかったか → 解決 → 教訓の型でまとめた実体験集です。同じ構成(Next.js + Vercel)で個人開発をしている方が、同じ落とし穴を踏む前に回避できることを狙っています。
筆者について:筆者はバックエンドエンジニア歴8年(Java Gold・ORACLE MASTER Gold 保有)の現役エンジニアであり、本業のかたわら Next.js + Vercel で複数の個人開発プロダクトを実際に運用しています。本記事で扱うトラブルはすべて筆者自身が運用中に踏んで解決したものです。技術的事実を正確に書くことを最優先し、誇張は避けています。なお、特定や混乱を避けるため、具体的なアプリ名・本番URL・秘密情報は本文には記載しません。
この記事の目次
- なぜ「ローカルで動く=本番で動く」ではないのか
- トラブル1:CSP × 静的プリレンダリングで全画面が真っ白
- トラブル2:Vercel がデプロイ自体をブロックする(commit author email not valid)
- トラブル3:Microsoft Clarity を入れたのに"無言ブロック"でデータが1件も取れない
- トラブル4:本番URLが別人に取られていた(vercel.app 同名ドメインの罠)
- 4つに共通する教訓:本番は"実機"で確かめるしかない
- よくある質問(FAQ)
- まとめ
なぜ「ローカルで動く=本番で動く」ではないのか
本題に入る前に、なぜこの手のトラブルが起きるのかを一段抽象化しておきます。これを理解しておくと、未知のトラブルにも当たりをつけやすくなります。
ローカル環境と本番環境では、動いている前提が違うことが多々あります。代表的には次のようなギャップです。
- レンダリングのタイミング:開発サーバー(
next dev)は基本的に毎回その場でレンダリングしますが、本番ビルド(next build)は一部のページをビルド時に静的生成(プリレンダリング)します。リクエスト時にしか手に入らない情報(ヘッダー等)を前提にすると、ここで挙動が割れます。 - ヘッダー・セキュリティ設定:本番でだけ付与される
Content-Security-Policy(CSP)などのセキュリティヘッダーは、ローカルで省略していると本番で初めて牙をむきます。 - デプロイ基盤(Vercel)のルール:ビルドが通ることと、Vercel がデプロイを受け付けることは別問題です。Vercel 独自のチェック(コミット作者メールの検証など)で止まることがあります。
- 外部サービスとの連携:解析タグや決済など、外部ドメインと通信するスクリプトは、ローカルで素通りしても本番のセキュリティ設定で弾かれることがあります。
そして最大の問題は、これらの多くがエラーを吐かずに"静かに"壊れることです。tsc --noEmit も next build も npm test も、これらのギャップを検知できません。型もテストも「コードの論理」は守れても、「本番という実行環境でのふるまい」までは保証しないからです。
ここからの4つは、まさにこの「静かに壊れる」典型です。
トラブル1:CSP × 静的プリレンダリングで全画面が真っ白
これがいちばん肝を冷やしました。セキュリティを上げる改修を入れた途端、本番のアプリ全体が反応しなくなったケースです。
症状
- ある画面は真っ白、別の画面は読み込みスピナーが回り続けて永遠に止まらない。
- ボタンを押しても何も起きない(クリックが効かない=完全に非インタラクティブ)。
- ローカルでは完全に正常。ビルドも型チェックもユニットテストも全部緑。
原因
セキュリティ強化のために nonce 方式の CSP を導入したのが引き金でした。nonce 方式とは、script-src 'strict-dynamic' 'nonce-<リクエストごとのランダム値>' のように、リクエストごとに変わるワンタイムの値(nonce)を持つ script だけ実行を許可する仕組みです。インラインスクリプトを使った XSS を防ぐ強力な方法です。
ところがここに罠があります。nonce はリクエストごとに変わる=サーバーが各リクエストで動的に生成して script に注入する必要があります。一方で Next.js は本番ビルド時に一部ページを静的プリレンダリングします。静的に作られたHTMLはビルド時点で確定しているので、後から来るリクエストの nonce を script に埋め込めません。
結果、ブラウザに届くのは「nonce の付いていない script」と「毎リクエストで変わる nonce を要求する CSP ヘッダー」の組み合わせ。両者が永遠に一致せず、_next/static/chunks/*.js がすべて CSP でブロック → React がハイドレーションできない → サイト全体が動かない、という全停止に至りました。
なぜローカルで気づけなかったか
- 開発サーバーは基本的に毎回動的レンダリングなので、nonce が正しく注入され、ローカルでは何の問題も起きません。
- 静的プリレンダリングは
next buildでしか発生せず、しかもnext build自体は成功します(ビルドエラーにならない)。 - 型チェックもユニットテストも、これは「ブラウザがCSPで script を実行できるか」というランタイム挙動なので原理的に検知できません。
- さらに厄介なのは、トップページのように静的HTMLだけでもそれっぽく見える画面は一見動いて見えてしまうこと。スピナーが回り続ける画面で初めて「あれ、おかしい」と気づきました。
解決
採れる手は大きく2方向あります。
- 対象ルートを動的レンダリングにする:ルートレイアウトでリクエストヘッダー(
headers())から nonce を読むようにすると、そのルートが動的化され、Next.js が生成する script に nonce が自動注入されます。手書きのインラインスクリプトにはnonce={nonce}を明示的に付けます。即効性はありますが、静的キャッシュを手放すぶん配信速度(TTFB)とはトレードオフになります。 - nonce 方式をやめてハッシュ方式(SRI)にする:静的生成を維持したまま strict な CSP を実現する手段です。Next.js には外部スクリプトに
integrity属性を自動付与する実験的機能があり、script-src 'self'のまま改ざん検知付きで配信できます。インラインスクリプトは外部ファイル化する必要があります。静的配信を保てるので速度面で有利ですが、設定難度はやや上がります。
どちらを採るにせよ、最後は必ず本番(または本番同等のプレビュー)で実機ブラウザを開いて確認しました。具体的には、ブラウザのコンソールで CSP 違反が0件であること、script にちゃんと nonce(または integrity)が付いていること、ボタンが実際に押せること(インタラクティブであること)を目視しました。
個人開発者への教訓
CSP やセキュリティヘッダーを変えたら、ビルド・型・テストが通っても"完了"にしない。実機ブラウザでハイドレーション生存(CSP違反0・要素が操作可能)を必ず確認する。 nonce 方式 CSP は「対象ルートが動的レンダリングであること」が前提だと覚えておくと、この事故は防げます。セキュリティ改修は"効いてほしい"あまり強くしすぎて自分のアプリを撃つことがある、という教訓でもあります。
トラブル2:Vercel がデプロイ自体をブロックする(commit author email not valid)
これは「コードは1行も悪くない」のにデプロイできなくなった、という理不尽系のトラブルです。原因を知らないと延々と別の場所を疑って時間を溶かします。
症状
vercel --prodを叩いても、ビルドが 0ms で終わって進まない、あるいはステータスが UNKNOWN のまま固着する。- デプロイログに
Deployment Blocked: commit author email not validのようなメッセージが出る。 - ローカルの
next buildは普通に成功する(=アプリ側は健全)。
原因
Vercel は CLI デプロイ(vercel --prod)であっても、git の直近コミット(HEAD)の作者メールアドレスを "deploy author" として読みます。このメールが Vercel アカウントに紐づく有効なものでないと、デプロイを丸ごとブロックします。
筆者がハマったのは、git の user.email が coo@local のような無効・未登録の値になっていたケースです(自動化スクリプトや別環境の既定IDが混入していました)。空メールでも同様に弾かれます。アプリのコードやビルド設定をいくら見直しても直らないのは当然で、止めていたのは git のメタデータ(コミット作者)だったわけです。
なぜローカルで気づけなかったか
- ローカルのビルドはコミット作者メールを一切見ないので、
next buildは通ります。 - このチェックはVercel 側だけで走る本番デプロイ固有のルールです。手元では再現しません。
- ビルドが「0ms」「UNKNOWN」と曖昧な失敗の仕方をするため、原因がアプリなのか設定なのか切り分けづらく、最初は別の所(環境変数やキャッシュ)を疑いがちです。
解決
git config user.emailを確認し、Vercel / GitHub に登録済みの有効なメールアドレスに統一する(リポジトリ単位とグローバル両方を揃える)。- すでに無効メールで積んでしまったコミットがある場合、HEAD の作者を是正する必要があります。手早いのは空コミットで HEAD の作者を上書きする方法(
git commit --allow-emptyを有効メール設定下で打つ)です。 - その上で再デプロイ。なお、デプロイが詰まったときに再デプロイを連打するのは逆効果です。レート制限を招いて状況が悪化することがあるので、1回失敗したら手を止めて原因を切り分けるのが鉄則です。
個人開発者への教訓
「ビルドは通るのにデプロイが進まない」ときは、コードより先に git のコミット作者メールを疑う。 特に複数環境(手元・CI・遠隔セッション)で作業していると、無効な既定メールが混入しがちです。デプロイ前に git config user.email をひと目確認する習慣をつけるだけで、この半日コースは回避できます。
トラブル3:Microsoft Clarity を入れたのに"無言ブロック"でデータが1件も取れない
「ちゃんと埋め込んだはずなのに、管理画面にデータが1件も入ってこない」。エラーも出ないので、いちばん気づくのが遅れたトラブルです。
症状
- アクセス解析・ヒートマップツール(Microsoft Clarity)のタグを正しく埋め込み、IDも設定した。
- なのに Clarity の管理画面にセッションが1件も記録されない。
- ブラウザでページを開いても、表面上は何のエラーも出ていない(無言)。
原因
ここでも CSP が犯人でした。Clarity の計測タグはページ表示後に外部ドメイン(clarity.ms など)からスクリプトを読み込み、*.clarity.ms や c.bing.com へデータを送信します。ところが CSP の許可リストにこれらのドメインを入れていなかったため、読み込み・送信がブラウザに静かにブロックされていました。
CSP 違反はアプリのJSの例外としては飛んでこない(コンソールに警告は出ても、アプリは何事もなく動き続ける)ので、「動いているのにデータだけ来ない」という気づきにくい壊れ方になります。
なぜローカルで気づけなかったか
- そもそも本番でしか計測タグを有効化していないと、ローカルでは検証対象にすらなりません。
- 「生HTMLを
curlで見て script タグがあるか」だけでは判定できません。この種の解析タグはページ読み込み後(afterInteractive相当)に注入されるため、生HTMLには現れないのが正常です。つまり curl で見えないことは「壊れている証拠」でも「正常の証拠」でもなく、判断材料になりません。 - データが「来ない」ことは、しばらく経って管理画面を見て初めて気づきます。能動的に確認しないと、何週間も無計測のまま放置してしまいます。
解決
- CSP の許可リストに計測サービスのドメインを追加する。Clarity の場合は、
script-srcにhttps://www.clarity.ms、connect-srcにhttps://*.clarity.ms https://c.bing.com(必要に応じてimg-srcも)を加えます。 - 検証は必ず実機ブラウザで行う。ページを開いてコンソールに CSP 違反が出ていないか、そしてツールの管理画面に実際にセッションが入ってくるかを確認します。curl では確認できません。
- 録画・ヒートマップ系のツールは、プライバシーポリシーへの開示も必要になります(誰が何のデータをどう扱うか)。計測の前にこの整備も忘れないようにします。
個人開発者への教訓
外部サービスのスクリプトを入れたら「埋め込んだ=動いている」と思い込まない。CSP の許可漏れで無言ブロックされていないか、実機で"データが実際に入っているか"まで確認する。 解析・広告・決済など、外部ドメインと通信するものは特に、CSP の許可リストとセットで考える癖をつけると事故が減ります。
トラブル4:本番URLが別人に取られていた(vercel.app 同名ドメインの罠)
最後は、技術というより運用の取り違えで痛い目を見たケースです。地味ですが、検索流入や被リンクに直結するので軽視できません。
症状
- 自分のアプリを
<アプリ名>.vercel.appで公開したつもりだったが、そのURLを開くと自分のものではない、まったく無関係のサイトが表示される。 - 自分の本番は、実際には
<アプリ名>-xxxx.vercel.app(末尾に文字列が付いた別名)で配信されていた。 - このことに気づかず短い方のURLを共有・登録していると、確認や計測がすべてずれる。
原因
*.vercel.app のサブドメインは早い者勝ちで、世界中の Vercel ユーザーと共有の名前空間です。狙った短い名前(例:<アプリ名>.vercel.app)がすでに他人のアカウントに取得済みだと、自分はその名前を使えず、Vercel が自動で <アプリ名>-xxxx.vercel.app のような別名を本番URLに割り当てます。
つまり「短くてきれいなURL」が自分のものとは限らない。同名の他人のサイトを自分のものと勘違いして、そちらを正規URLとして扱ってしまうと事故ります。
なぜローカルで気づけなかったか
- これはローカル開発とは無関係で、公開フェーズで初めて表面化する問題です。
- 自分の管理画面(Vercel ダッシュボード)で見える本番URLと、頭の中で「こうなっているはず」と思い込んでいるURLが食い違っていると、確認時に別サイトを叩いてしまいます。
- 別サイトが普通に表示されてしまうので、「デプロイが反映されていない」と誤診して、無駄に再デプロイを繰り返す二次被害も起きます(実際は正しいURLを見ていないだけ)。
解決
- Vercel ダッシュボードで自分のプロジェクトの本番URL(Production Domain)を正確に確認し、それを唯一の正規URLとして扱う。
- 検索エンジン登録(Google Search Console)・被リンク・SNS や記事での共有リンクを、すべてその正しいURLに統一する。同名の別サイトに登録してしまうと、所有権確認も計測も成立しません。
- デプロイ反映の確認は、必ず自分の正規URLに対して行う(末尾の文字列まで含めて正しいか確認する)。別サイトを叩いて「未反映だ」と誤判定しないようにします。
- 長期運用するなら、独立したカスタムドメインを取得して紐づけてしまうのが、この手の取り違えを根本から断つ確実な方法です。
個人開発者への教訓
*.vercel.app の短い名前は自分のものとは限らない。本番URLは"思い込み"でなくダッシュボードの実値で確認し、検索登録・被リンク・共有は必ずその正規URLに統一する。 きれいな短いURLに執着せず、管理画面が示す本物のURLを正とするだけで、計測ずれや誤判定を防げます。
4つに共通する教訓:本番は"実機"で確かめるしかない
4つのトラブルを並べると、共通する根っこが見えてきます。
- どれも「型・ビルド・ユニットテストが緑」では検知できなかった。これらはコードの論理は守れても、本番という実行環境でのふるまい(ブラウザがJSを実行できるか、Vercel がデプロイを受け付けるか、外部通信が通るか、どのURLが自分のものか)までは保証しません。
- どれも"静かに"壊れる。例外が飛ばず、見た目は動いているのにデータが来ない・操作できない・別物が表示される、という気づきにくい壊れ方をします。
- どれも実機で能動的に確認すれば防げた。本番(またはプレビュー)のブラウザを開き、コンソールのCSP違反を見て、ボタンを押し、管理画面にデータが入るか・正しいURLか、を自分の目で確かめること。これが唯一の確実な防御線でした。
個人開発はレビュアーが自分ひとりです。だからこそ、CIの緑だけで安心せず、「本番で実際に触って確かめる」工程を必ず1ステップ挟むことを、強くおすすめします。特に、CSP などのセキュリティ設定・デプロイ基盤の挙動・外部サービス連携・公開URLの4点は、ローカルと本番でギャップが生まれやすい鬼門だと意識しておくと、被害を未然に防げます。
よくある質問(FAQ)
Q. Next.js + Vercel で本番だけ画面が真っ白になるのはなぜですか?
A. 原因は複数ありますが、よくあるのが CSP(Content-Security-Policy)と静的プリレンダリングの噛み合わせです。nonce 方式の CSP を使うとリクエストごとに nonce が変わるため、ビルド時に固定生成された静的HTMLの script に正しい nonce が付かず、ブラウザが全JSをブロックしてハイドレーションが止まります。型チェック・ビルド・ユニットテストは通ってしまうので、実機ブラウザで CSP 違反が0件か必ず確認してください。
Q. Vercel の「Deployment Blocked: commit author email not valid」は何が原因ですか?
A. git の直近コミット(HEAD)の作者メールアドレスが、Vercel アカウントに登録されていない、または無効な値(例:coo@local や空)になっていることが原因です。Vercel は CLI デプロイでもコミット作者メールを deploy author として読むため、無効だとデプロイ自体をブロックします。git config user.email を、Vercel/GitHub に登録済みの有効なメールに統一し、必要なら空コミットで HEAD の作者を是正してから再デプロイします。
Q. Microsoft Clarity を入れたのにデータが取れません。なぜですか?
A. CSP の許可リストに Clarity のドメインが入っていないと、計測スクリプトの読み込みや送信がブラウザに無言でブロックされます。script-src に https://www.clarity.ms、connect-src に https://*.clarity.ms https://c.bing.com を追加してください。検証は実機ブラウザで、コンソールに CSP 違反がないか・管理画面にセッションが入るかで行います。生HTMLの curl では判定できません(タグはページ読み込み後に注入されるため)。あわせてプライバシーポリシーへの開示も必要です。
Q. 狙っていた <アプリ名>.vercel.app が使えないのですが、どうすればいいですか?
A. 短い vercel.app のサブドメインは早い者勝ちで、既に他人のアカウントが取得済みのことがあります。その場合あなたの本番は <アプリ名>-xxxx.vercel.app のような別名になります。重要なのは、Google Search Console への登録・被リンク・SNS や記事で共有するURLを、必ず自分が管理している正しいURLに統一することです。同名の別サイトを自分のものと取り違えると、確認や計測が丸ごとずれます。長期運用するならカスタムドメインを取得して紐づけるのが確実です。
Q. 型チェックもテストも通ったのに本番が壊れるのを、どうすれば事前に防げますか?
A. 型・ビルド・ユニットテストは「コードの論理」は守れても「本番という実行環境でのふるまい」までは保証しません。CSP・デプロイ基盤・外部サービス連携・公開URLは特にローカルと本番でギャップが出やすい領域です。CIが緑でも、本番(またはプレビュー)のブラウザを開き、コンソールのCSP違反・操作可否・解析データの記録・正しいURLを自分の目で確認する工程を必ず1ステップ挟むのが確実な防御です。
まとめ
Next.js + Vercel の個人開発で実際に踏んだ「本番だけ静かに壊れる」トラブルを4つ整理しました。
- CSP × 静的プリレンダリングで全画面真っ白:nonce 方式 CSP は対象ルートが動的レンダリング前提。静的HTMLには nonce が付かず全JSブロック。実機でCSP違反0を確認する。
- Vercel がデプロイをブロック(commit author email not valid):Vercel は CLI デプロイでもコミット作者メールを読む。無効メールだとブロック。有効なメールに統一して是正する。
- Clarity が無言ブロックでデータゼロ:CSP に計測ドメインの許可がないと静かにブロックされる。許可リスト追加+実機でデータ記録を確認する。
- 本番URLが別人に取られていた:短い vercel.app 名は早い者勝ち。ダッシュボードの実値を正規URLとし、検索登録・被リンク・共有を統一する。
4つに共通するのは、どれも型・ビルド・テストでは検知できず、エラーも出さずに"静かに"壊れること。そして、本番(またはプレビュー)を実機で能動的に確認すれば防げたことです。
個人開発はレビュアーが自分ひとりだからこそ、CIの緑で満足せず「本番で実際に触って確かめる」工程を1つ挟む。これが、ローカルでは見えないトラブルから自分のプロダクトを守る、いちばん確実な習慣です。
免責:本記事は筆者の実体験および一般に公開されている情報にもとづく情報提供です。Next.js・Vercel・Microsoft Clarity 等の仕様や挙動はバージョン・時期により変わる可能性があるため、実装にあたっては各公式ドキュメントの最新情報をご確認ください。記載の解決策はあくまで一例であり、すべての環境で同じ結果を保証するものではありません。