ローカルでは動くのに本番だけ落ちる
「自分の環境では動く」を、勘ではなく二分法で潰せるようになる。環境差を7カテゴリに分類して1つずつ消し込む手順と、差分を機械的に列挙するコマンドで、原因を数十分で特定できる。
- この症状の本質は「動く環境と動かない環境の差分は必ず有限で、列挙できる」こと。勘で当てにいかず、差分の集合を機械的に出してから二分する。
- 差分は7カテゴリに収まる。設定値・依存バージョン・ファイルシステム(大文字小文字)・ロケールとタイムゾーン・実行ユーザーと権限・リソース制限・外部依存の到達性。
- 最強の一手は「本番の環境変数でローカルを起動する」「ローカルのイメージを本番で動かす」のどちらか。片方を相手に寄せて再現させれば、差分の半分が一度に消える。
「手元では完全に動く。CIも通る。なのに本番でだけ落ちる」。この症状が消耗戦になりやすいのは、再現できない相手をデバッグしようとするからです。ログを眺めて仮説を立て、直したつもりでデプロイし、また落ちる——このループは1回転に十数分かかるので、当てずっぽうでは日が暮れます。
突破口は発想の転換にあります。動く環境と動かない環境の差分は必ず有限で、列挙できる。当てにいくのではなく、差分の集合を洗い出してから二分するのが最短経路です。
まず「どちらを相手に寄せるか」を決める
横にスクロール
個別の差分を潰す前に、最も効率のよい一手を先に打ちます。片方の環境をもう片方に近づけて再現させることです。どちらの向きでも構いませんが、性質が違います。
| 寄せ方 | やること | 一度に消える差分 | 向く状況 |
|---|---|---|---|
| 本番→ローカル | 本番の環境変数・設定でローカルを起動する | 設定値・外部依存の向き先 | 設定起因が疑わしい。最も安全で速い |
| ローカル→本番 | 手元と同じイメージ・同じタグを本番で動かす | 依存バージョン・OS・ファイルシステム | ビルド成果物やOS差が疑わしい |
| 本番の中で再現 | 本番と同じイメージにシェルで入り手で実行する | オーケストレータ由来の差以外すべて | アプリ単体か環境かを分けたい |
本番の環境変数には、ほぼ確実に本番データベースの認証情報や外部APIのキーが含まれます。そのままローカルで起動すると、手元のコードが本番データを書き換えます。持ってくるのは接続先を除いた設定に限る、あるいは接続先をステージングへ差し替える、のどちらかを必ず行ってください。実際に「ローカルで再現させようとして本番のデータを消した」は、この症状の調査中に起きる事故の定番です。
差分は7カテゴリに収まる
寄せても再現しないなら、残りを1つずつ消し込みます。経験上、この症状の原因はほぼ次の7つに収まります。
1. 設定値(最頻)
環境変数の未設定、綴り違い、空文字と未設定の区別、型("false" という文字列が真と評価される)。まず両環境の環境変数を実際に列挙して差分を取るのが確実です。
# 両環境で同じ形式に整えてから diff する
env | sort > /tmp/env-prod.txt
diff <(sort /tmp/env-local.txt) /tmp/env-prod.txt
設定ファイルの読み込み順(既定値→ファイル→環境変数)が環境ごとに違って、意図しない値が勝っていることもあります。アプリの起動時に、解決後の実効設定をログへ出すようにしておくと、この手の調査が一瞬で終わります。
2. 依存バージョン
ロックファイルを使っていても、ネイティブ拡張やベースイメージの共有ライブラリまでは固定されません。「同じ package.json なのに挙動が違う」場合、疑うのはロックファイルの適用漏れ(npm install と npm ci の違い)と、C/C++ の共有ライブラリのバージョン差です。
# 実際にロードされている共有ライブラリを見る
ldd $(which node) 2>/dev/null | head
3. ファイルシステムの大文字小文字
macOS と Windows の既定は大文字小文字を区別せず、Linux は区別します。 これが「手元では動くのに本番(Linux コンテナ)でだけモジュールが見つからない」の古典的な原因です。
import UserService from './userService' // 実ファイルは UserService.ts
→ macOS: 解決できる
→ Linux: Cannot find module で落ちる
Git は既定でファイル名の大文字小文字の変更を追跡しにくいため、リネームがリポジトリに反映されていないことにも気づきにくい厄介な差です。
4. ロケールとタイムゾーン
コンテナの既定は UTC かつ LC_ALL=C であることが多く、開発機は現地時間・日本語ロケールです。日付の解釈、文字列のソート順、数値の小数点記号(カンマかピリオドか)、そして大文字化の挙動(トルコ語ロケールでの i の扱いは有名な罠)が変わります。「日付をまたぐ処理だけ本番で結果が違う」なら、まずここを疑います。
5. 実行ユーザーと権限
本番コンテナは非 root で動かすのが定石なので、手元では root で通っていた書き込みが本番では通りません。書き込み先が読み取り専用ファイルシステムだったり、一時ディレクトリのパスが違ったりもします。個別の症状はEACCES(Permission denied)にまとめてあります。
6. リソース制限
コンテナのメモリ上限、ファイルディスクリプタ上限、CPU 割り当て、PID 上限。手元は実質無制限なので、制限に当たって初めて出るバグは本番でしか見えません。EMFILE(Too many open files)やOOMKilled / 終了コード137はこの系統です。
# コンテナ内から見た上限を確認する
ulimit -a
cat /sys/fs/cgroup/memory.max 2>/dev/null || cat /sys/fs/cgroup/memory/memory.limit_in_bytes
7. 外部依存の到達性
手元からは見えるが本番のサブネットからは見えない、あるいはその逆。プロキシ設定、DNS の解決先の違い、mTLS のクライアント証明書の有無。到達性そのものの切り分けはpingは通るのにcurlが失敗するの手順がそのまま使えます。
差分を潰す順番
7つを闇雲に見るのではなく、エラーの出方から入口を選びます。
| 症状の出方 | 最初に見るカテゴリ | 理由 |
|---|---|---|
| 起動直後に即落ちる | 設定値・実行権限 | 設定の読み込みとファイル配置は起動時に確定する |
| モジュールやファイルが見つからない | 大文字小文字・依存バージョン | Linuxとの差が最も出やすい層 |
| しばらく動いてから落ちる | リソース制限 | 上限は蓄積してから当たる |
| 特定の日時・特定の入力でだけ | ロケール・タイムゾーン | 条件依存はデータの解釈差が疑わしい |
| 外部呼び出しだけ失敗する | 到達性・プロキシ | アプリのロジックは無罪の可能性が高い |
この症状の調査で最も価値が高い成果は「修正」ではなく「手元で確実に再現する手順」です。再現さえすれば、あとは通常のデバッグでしかありません。逆に再現できないまま推測で直すと、直ったかどうかを本番デプロイでしか確かめられず、1回転が極端に遅くなります。時間の配分は「再現に8割、修正に2割」でちょうどよいくらいです。
再発を防ぐ設計
- 本番と同じイメージで開発する。 差分カテゴリの2・3・4・5がまとめて消えます。この症状に対する最も費用対効果の高い投資です。
- 起動時に実効設定をログへ出す。 秘密情報はマスクしたうえで、解決後の設定値と参照した設定ファイルのパスを出力します。
- 必須の環境変数は起動時に検証して即座に落とす。 未設定のまま起動して、その値を使う機能が呼ばれた瞬間に初めて落ちる、という遅延した失敗が調査を難しくします。
- CI をできるだけ本番に似せる。 CI が macOS で本番が Linux なら、大文字小文字の差は CI をすり抜けます。
まとめ
- この症状の本質は「差分は有限で列挙できる」こと。勘で当てず、差分の集合を出してから二分する。
- 最初の一手は片方の環境をもう片方へ寄せて再現させること。設定を持ってくる際は接続先を必ず差し替える。
- 差分は7カテゴリ——設定値・依存バージョン・大文字小文字・ロケールとタイムゾーン・実行ユーザーと権限・リソース制限・外部依存の到達性。
- エラーの出方から入口のカテゴリを選ぶ。起動直後なら設定、しばらく経ってからならリソース制限。
- 調査の成果物は修正ではなく確実な再現手順。再現できれば残りは通常のデバッグに落ちる。
トラブルシュート実戦の記事ガイド
ローカルでは動くのに本番だけ落ちるを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
トラブルシュート
比較で見る軸
難易度: advanced / カテゴリ: トラブルシュート実戦 / タグ数: 5
導入後に効く点
差分は7カテゴリに収まる。設定値・依存バージョン・ファイルシステム(大文字小文字)・ロケールとタイムゾーン・実行ユーザーと権限・リソース制限・外部依存の到達性。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- トラブルシュート実戦
- タグ数
- 5
判断チェックリスト
- 自社の用途が「トラブルシュート / デプロイ」に近いか確認する。
- 強みである「この症状の本質は「動く環境と動かない環境の差分は必ず有限で、列挙できる」こと。勘で当てにいかず、差分の集合を機械的に出してから二分する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。