結論:Next.jsのアプリをデプロイしたら、本番の画面が真っ白になりました。原因は、pm2で再起動したつもりのアプリが、実は1か月半前から一度も入れ替わっていなかったことです。pm2で npm start を起動していると、pm2 restart をしても実際のアプリ(子プロセス)が古いまま残ることがあります。再起動できたかどうかは、pm2の表示ではなく「ポートを使っているプロセスの起動時刻」で確かめるのが確実です。
何が起きたか
自作のFIRE(早期リタイア)シミュレーションのアプリ(Next.js 16)に、年金・税金・相場シナリオなどの機能を足して、VPSに出しました。手順はいつもどおりです。
git pullで最新のコードを取り込むnpm run buildでビルドするpm2 restart fire-trackerで再起動する
curl http://localhost:3000 は200を返し、ログにも「Ready」と出ています。ところが、ブラウザで本番のサイトを開くと画面が真っ白でした。
原因の切り分け
1. HTMLは返っている。でも一部のJS・CSSが500エラー
本番のHTMLを取得して、中で読み込んでいる /_next/static/chunks/ のファイルを1つずつ確かめました。
curl -s https://(本番のドメイン)/ | grep -oE '/_next/static/[^"]+\.(js|css)' | sort -u
8個のうち2個(JSとCSS)が500エラーでした。画面を作るファイルが読めないので、真っ白になります。また、HTMLには今回追加したはずのメニューが入っていませんでした。
2. nginxの送り先が違う? → 違った
「別のポートで古いアプリが動いていて、nginxがそちらに送っているのでは?」と疑いました。サーバーの中で curl http://localhost:3000 のHTMLを見ると、本番とまったく同じファイル名を読みにいっていました。つまり、本番はちゃんと3000番のアプリにつながっています。
3. ファイルはディスクにない。プロセスの起動時刻は1か月半前
決め手になったのは次の3つのコマンドです。
# HTMLが指しているファイルが、今のビルドにあるか
ls .next/static/chunks | grep (500になったファイル名)
# 3000番を使っているプロセスの起動時刻
ss -ltnp | grep ":3000"
ps -o pid,lstart,cmd -p (pid)
# 今のビルドを作った時刻
ls -l --time-style=full-iso .next/BUILD_ID
結果はこうでした。
| 確かめたこと | 結果 |
| 500になったファイルは今のビルドにあるか | ない |
| 3000番のプロセスの起動時刻 | 9月22日 |
| 今のビルドを作った時刻 | 11月6日 |
pm2の created at |
9月22日のまま |
動いていたのは9月22日のビルドのアプリでした。そのアプリが返すHTMLは古いファイル名を指しています。ところが新しくビルドしたときに古いファイルは消えているので、500エラーになっていたわけです。9月22日より後のデプロイも、実は反映されていなかった可能性があります。
なぜ pm2 restart が効かなかったのか
pm2には pm2 start npm --name fire-tracker -- start のように、npm経由で登録していました。この形だと、プロセスはこういう親子関係になります。
pm2
└ npm start(pm2が管理しているのはここ)
└ next start → next-server(実際に3000番を使っているのはここ)
pm2 restart で親のnpmだけが止まり、子の next-server が3000番を持ったまま残ると、新しく起動したほうはポートを取れません。そして古いアプリがそのまま返事をし続けます。curl は200を返すし、pm2の表示も「online」なので、気づきにくいのが厄介なところです。
※ 子プロセスが残った正確なきっかけまでは突き止められていません。ただ、プロセスの起動時刻がビルドより前で、pm2の created at も古いままだったことから、「再起動が実際のアプリに届いていなかった」ことは確かです。
直し方
pm2の登録を消し、残っていた古いプロセスを止めてから、npmを通さずNext.jsを直接起動し直しました。
pm2 delete fire-tracker
kill (古いプロセスのpid)
ss -ltnp | grep ":3000" # 何も出なければ空いている
pm2 start node_modules/next/dist/bin/next --name fire-tracker -- start -p 3000
pm2 save
これで、本番のJS・CSSは8個すべて200になり、新しい画面が表示されました。次のデプロイでは pm2 restart の前後でプロセスの起動時刻が変わり、ちゃんと入れ替わることも確かめています。
再発防止:再起動できたかは「起動時刻」で確かめる
いまはデプロイのたびに、pm2 restart の前と後でこれを実行しています。
ps -o pid,lstart,cmd -p $(ss -ltnp | grep ":3000" | grep -oE "pid=[0-9]+" | cut -d= -f2)
pidと起動時刻が変わっていれば入れ替わっています。変わっていなければ、古いアプリが残っています。pm2の「online」や curl の200だけでは、新しいアプリが動いている証拠にはならない、というのが今回の一番の学びでした。
おまけ:ログに出ていた「Server Reference ID」のエラーは無関係
調べている途中、pm2のエラーログに次のようなエラーが大量に出ていて焦りました。
Error: The Server Reference ID did not match the expected format. Received "x".
これは、存在しないServer Actionを呼ぼうとするリクエストをNext.jsが断ったときの記録です。インターネット上で脆弱性を探して回るボットが送ってくるもので、今回の真っ白とは関係ありませんでした。Server Actionを使っていないアプリで、Next.js・Reactが新しい版になっていれば、入り口で断られるので心配はいりません。
まとめ
- デプロイ後に真っ白なら、まずHTMLが読み込むJS・CSSが500や404になっていないかを見る
- pm2で
npm startを起動していると、pm2 restartで古いアプリが残ることがある - pm2には、npmではなくNext.jsを直接登録する
- 再起動できたかは、ポートを使っているプロセスの起動時刻で確かめる
今回直したアプリはこちらです。年金・税金・相場が悪かったときのシナリオまで入れて、何歳まで資産が持つかを試算できます。

コメント