自宅サーバーでNext.jsを動かしていると、必ず「デプロイしたのに古いページが出る」に当たります。 これが厄介なのは、原因が1つではないことです。コミットが届いていない、デプロイが走っていない、 ビルドキャッシュが古い、前段のCDNが古い。どれも症状はまったく同じ「更新されない」になります。 闇雲に再デプロイを繰り返すと、運良く直ることもありますが、次に同じことが起きたときに また同じ時間を捨てることになります。
なので、確認する順番を固定しました。下流から疑うと時間が溶けるので、上流から1層ずつ潰すのがコツです。
層1: pushが本当に届いているか
最初に踏んだのがこれでした。ローカルではコミットできているのに、 デプロイ基盤がビルドしているのは1つ前のコミット。git pushを実行したとき、ブランチのupstreamが未設定だと失敗します。 ところがエディタのUIから操作していると、その失敗が見えないまま次の作業に進んでしまい、 「pushした」という記憶だけが残ります。
確認は2つの値を並べるだけです。ローカルのgit rev-parse HEADと、 リモートのgit ls-remote origin master。この2つが違えば、 その時点でデプロイを疑う必要はありません。 初回だけgit push --set-upstream origin masterで明示的に紐づけておくと、 以降は素のgit pushが通ります。
もう1つの落とし穴が、違うブランチにpushしているパターンです。 ローカルではmain、リモートのビルド設定はmasterを見ている、といった状態。 これはgit status -sbの1行目(## master...origin/master)を見る習慣を つけるだけで防げます。
層2: デプロイが走り、そのコミットを掴んだか
pushが正しくても、デプロイが走っていないことがあります。 セルフホストのデプロイ基盤は「Git連携で自動ビルド」と「APIから手動トリガー」の両方を 持っていることが多く、片方だけ設定されているケースが普通にあります。
ここで見るべきは「成功したか」ではなく「どのコミットをビルドしたか」です。 デプロイの一覧には、だいたいcommitやrevisionのフィールドがあります。 これが自分のHEADのSHAと一致していなければ、成功していても意味がありません。 一致していない場合は、トリガーした対象(アプリのUUIDなのか、デプロイのUUIDなのか)を 取り違えていることが多いです。ポーリング先を間違えると「見つからない」が返り続けるので、 ここで詰まると無限に待つことになります。
層3: ビルドキャッシュ
commitが一致しているのに挙動が変わらない場合、次はビルド成果物を疑います。 Next.jsはビルドキャッシュを積極的に使うので、依存関係の解決結果や設定の変更が反映されないことがあります。 特にoutput: standaloneで動かしていると、古い.nextがそのまま コンテナに入り、コードだけ新しくなって設定が古い、という噛み合わせの悪い状態が作れます。
切り分けは単純で、キャッシュを使わないビルドを1回走らせるだけです。 それで直るなら原因はここで確定です。 「再起動しただけ」では直らないのもこの層の特徴で、 コンテナの再起動はプロセスを立て直すだけで、イメージの中身は変わりません。
層4: 前段のキャッシュ
ここまで全部正しくても、まだ古いページが出ることがあります。 リバースプロキシやCDNがエッジで返している場合です。 Cloudflareを前段に置いているなら、レスポンスヘッダのcf-cache-statusを見ればHITなのかMISSなのかが分かります。
確認するときは、URLに意味のないクエリを1つ足してキャッシュキーを変えます。?cb=にランダムな数字を付けて開き、それで新しい内容が出るなら、 アプリケーションは正しくてエッジが古い、と一発で確定します。 逆にクエリを付けても古いままなら、疑うべきは層3より上です。
チェックリスト
git rev-parse HEADとgit ls-remote origin masterが一致しているか- ビルドされたコミットのSHAが、そのHEADと一致しているか
- キャッシュ無効のビルドで直るか(直るならビルドキャッシュ)
?cb=付きで直るか(直るならエッジキャッシュ)- 「再起動」と「再ビルド」を混同していないか
この4層を上から順に見るようにしてから、原因不明のまま再デプロイを繰り返すことが なくなりました。地味ですが、デプロイ周りで一番効く習慣だと思っています。