Claude Media
Claude Codeでexit code 137のOOMを切り分ける手順 — dockerとdmesg

Claude Codeでexit code 137のOOMを切り分ける手順 — dockerとdmesg

コンテナがexit code 137で落ちたとき、docker inspectのOOMKilledとdmesgのkillログを突き合わせ、上限不足かリークかを使用量の推移で見分ける手順です。

exit code 137は、コンテナ内のプロセスがSIGKILLで終わったことしか教えてくれません。メモリ不足で殺されたのか、docker stop の猶予切れなのか、誰かが kill したのかは、docker inspect の OOMKilled とカーネルログを見て初めて分かります。Claude Codeにこの調査を任せるなら、読み取りだけの権限で動かし、証拠を3か所から集めさせる形が安全です。

137は「SIGKILLで終わった」までしか言わない

137は128に9を足した値です。Bashは、致命的なシグナルNで終了したコマンドの終了ステータスを128+Nとします。シグナル9はSIGKILLなので、137は「SIGKILLを受けて終わった」という意味になります。

Docker側の規則も押さえておきます。docker run の終了ステータスのうち、125・126・127はDocker自身や起動の失敗を表します。それ以外はコンテナ内コマンドの終了コードがそのまま返ります。137は後者で、コンテナ内のプロセスが実際にSIGKILLを受けた結果です。

SIGKILLを送る側は複数あります。

  • カーネルのOOM killer(メモリ不足による強制終了)
  • docker stop の猶予時間(--timeout)切れ。SIGTERMのあと、猶予内に終わらなければSIGKILLが送られる
  • 手動の docker kill や kill -9

最初の一手は、この3つのどれかを決めることです。メモリが原因なら次の節に進み、そうでなければ停止処理やオーケストレーターの設定を疑います。

docker inspectのOOMKilledとExitCodeを読む

コンテナの状態には OOMKilled が入っています。コンテナを最後に起動してから、中のプロセスがメモリ不足で殺されたかどうかを示す真偽値です。ExitCode や FinishedAt も同じ State の下にあります。

docker inspect \
  --format 'oom={{.State.OOMKilled}} exit={{.State.ExitCode}} finished={{.State.FinishedAt}}' \
  myapp
docker inspect --format 'limit={{.HostConfig.Memory}} restarts={{.RestartCount}}' myapp

出力の読み方は次の表のとおりです。

oomexit読み取れること
trueexit137読み取れることメモリ不足で殺された。上限不足かリークかは次の節で分ける
falseexit137読み取れることOOM以外のSIGKILL。docker stop の猶予切れや手動killを疑う
falseexit0や1など読み取れること137ではない。アプリの正常終了か通常のエラー

HostConfig.Memory はメモリ上限をバイトで返し、既定値は0です。コンテナにはもともと資源の制約がなく、-m を付けて初めて上限が掛かります。0のままOOMKilledがfalseで137なら、上限を超えたのではなく、別の理由か、ホスト全体のメモリ不足を考えます。

RestartCount も見ます。再起動ポリシーがあると、落ちて戻るたびにカウントが増え、OOMKilled は直近の起動分だけを示すためです。落ちた回数を数えたいときは、この値と時刻を合わせて読みます。

dmesgで誰が殺したかを突き合わせる

OOMKilled はコンテナ単位の結論です。どのプロセスが殺されたか、そのときどれだけ使っていたかは、カーネルログに残ります。

sudo dmesg -T | grep -i -E 'oom|killed process'

出力には、強制終了されたプロセス名とPID、anon-rss(実メモリ使用量)が並びます。形式は次のようなものです。数値は説明用の仮の値で、カーネルのバージョンによって文言には差があります。

Memory cgroup out of memory: Killed process 4121 (node) total-vm:3412000kB, anon-rss:524288kB

見るのは3点です。

  • プロセス名が、コンテナのメインプロセスか子プロセスか
  • anon-rss が、docker inspect で見た上限に近いか
  • 時刻が、FinishedAt と合うか

時刻と上限の両方が合えば、そのコンテナの上限に当たって殺されたと言えます。ログが空のときは、権限で読めていないか、見ているカーネルが違う可能性があります。目当ての行があるのは、コンテナを動かしているカーネル側のログです。コンテナの実行環境がホストOSと別のカーネルなら、ホストOSの端末で叩いても出ません。

cgroup v2のホストでは、コンテナごとのcgroupにカウンタも残ります。memory.events の oom_kill は、そのcgroupに属するプロセスがOOM killerに殺された回数です。memory.peak は記録された最大使用量、memory.max はメモリの上限(既定は max)を示します。

docker exec myapp cat /sys/fs/cgroup/memory.events
docker exec myapp cat /sys/fs/cgroup/memory.peak /sys/fs/cgroup/memory.max

oom_kill が1以上で memory.peak が memory.max に張り付いていれば、上限に到達しています。ただしコンテナが残っている間しか読めません。--rm 付きで消えたあとはカーネルログだけが頼りになります。このファイル名はcgroup v2のものなので、先にホストのcgroupのバージョンを確かめてください。

docker eventsで次の発生を待ち受ける

再現が週に1回といった頻度だと、落ちたあとに調べても材料が足りません。docker events はコンテナの oom イベントを出せるので、フィルターを掛けて常駐させておくと、発生時刻をその場で記録できます。

docker events --filter event=oom --filter container=myapp

イベントの時刻は、dmesg のkill行と FinishedAt を結ぶ3つ目の目印になります。次の節で扱う docker stats のループを同時に回しておけば、落ちる直前の使用量が mem-trace.log の末尾に残ります。Claude Codeに任せる場合は、常駐コマンドは自分の端末で動かし、ログファイルだけを読ませるほうが権限の面でも単純です。

上限不足かリークかを使用量の時系列で見分ける

OOMと分かったら、次は原因です。1回の瞬間値では決まらないので、使用量を時間軸で取ります。

while true; do
  date +%T
  docker stats --no-stream --format '{{.Name}} {{.MemUsage}} {{.MemPerc}}' myapp
  sleep 10
done | tee mem-trace.log

docker stats の MEM USAGE / LIMIT は、使用中の合計と許可された上限を並べて返します。Linuxでは、Docker CLIは使用量からキャッシュ分を引いて表示します。cgroup v2のホストではキャッシュ分は memory.stat の inactive_file で決まるので、ホスト側のツールと数字が食い違うことがあります。傾向を読む目的なら、同じ指標を取り続ければ足ります。

推移の形ごとの読み方は次のとおりです。ここは編集上の経験則で、公式の判定基準ではありません。

推移の形疑うこと次の手
起動後すぐ上限付近まで上がり、横ばいで落ちる疑うこと上限が小さすぎる次の手通常運転のピークを測り、上限を見直す
時間に比例して右肩上がりに増え続ける疑うことリーク(解放されない確保)次の手プロファイラーで増えている箇所を探す
普段は低く、特定の処理で一気に跳ねる疑うこと1回の大きな確保次の手その処理の入力サイズを制限する
ノコギリ状に上下し、頂点が少しずつ上がる疑うこと解放が追いつかない次の手GC設定や接続・バッファの保持を疑う

上限不足なら直し方は単純で、通常運転で必要な量を測り、それに余裕を足して -m を決めます。リークなら上限を上げても落ちるまでの時間が延びるだけです。docker run の --memory は最小で6MBから指定でき、--memory-swap を --memory と同じ値にするとスワップは使えなくなります。スワップを許すとリークの進行が見えにくくなるので、調査中は同じ値にして素直に落とすほうが推移を追いやすくなります。

--oom-kill-disable で殺されないようにする手は使いません。上限を付けていないコンテナでこれを使うと、ホストがメモリ不足になったときにシステムのプロセスが殺される恐れがあると、Dockerのドキュメントが警告しています。原因の切り分けが済むまでは、落ちる状態のまま観測するのが正攻法です。

Claude Codeには読み取りだけを許して調べさせる

ここまでの手順は、どれも状態を読むコマンドです。Claude Codeに任せるなら、docker inspect と docker stats と dmesg だけを許可し、停止や削除は拒否しておきます。

{
  "permissions": {
    "allow": [
      "Bash(docker inspect *)",
      "Bash(docker stats --no-stream *)",
      "Bash(docker logs *)",
      "Bash(docker exec myapp cat /sys/fs/cgroup/*)",
      "Bash(sudo dmesg *)"
    ],
    "deny": [
      "Bash(docker rm *)",
      "Bash(docker kill *)",
      "Bash(docker stop *)",
      "Bash(docker update *)"
    ]
  }
}

この設定には、権限の仕様から来る注意点が4つあります。

  • ルールは、許可・確認・拒否の順に評価されます。denyはallowより優先され、allowで例外は作れません。
  • sudo は、ルール照合の前に取り除かれるラッパーの一覧(timeout・time・nice・nohup・stdbuf など)に入っていません。dmesg を許可するなら、上のように sudo dmesg まで含めて書きます。
  • パイプでつないだコマンドは、サブコマンドごとに別々に照合されます。sudo dmesg -T | grep oom は、sudo dmesg のルールと、grep の読み取り専用判定の両方を通ります。
  • denyは、/usr/bin/docker kill や docker -H host kill のような別の書き方を止めません。破壊的な操作を確実に止めたいときは、sandboxかPreToolUse hookと組み合わせます。

別のデーモンを指す -H や --context を付けた docker は、読み取り専用の形でも確認を求められます。リモートのコンテナを調べるときは、プロンプトが出るのが仕様どおりの動きです。許可ルールはClaude Codeが強制するもので、CLAUDE.md や依頼文での指示は権限を変えません。

そのうえで、依頼文には調べる順序と、してはいけないことを書きます。

コンテナ myapp が exit code 137 で落ちる。次の順で調べて報告して。
1. docker inspect で OOMKilled・ExitCode・FinishedAt・上限・RestartCount を取る
2. sudo dmesg で FinishedAt の時刻に合う kill 行を探す
3. docker stats を10秒間隔で3分間取り、増え方の形を表にする
コンテナの停止・削除・再作成と、上限の変更はしない。変更案は提案だけ書く。

Claude Codeが返すのは、取得した数値と、増え方の分類です。分類の根拠は mem-trace.log の数字で示させ、数字のない断定は受け付けない運用にします。再現のたびに結果が変わる調査なので、ログをファイルに残しておくと、別の日に比べ直せます。

docker をsandboxの外で動かす設定が要る環境もあります。その場合の対象外指定はsandbox.excludedCommandsでdockerなど非対応コマンドを対象外にするにまとめています。コンテナ内でClaude Codeを動かしている側の設定はClaude Code Docker実行ガイドが扱っています。

切り分けた結果ごとの直し方

結果は4通りに落ち着きます。

結果直し方
上限が小さい直し方--memory を測ったピークに余裕を足して引き上げる。--memory-reservation は上限より低くしか置けず、保証にはならない
リーク直し方上限は据え置きでプロファイラーを当てる。再起動ポリシーは延命策として使う
OOMではない137直し方docker stop の猶予を延ばす(--stop-timeout)か、SIGTERMを受けて終われるようアプリを直す
コンテナではなくホストが不足直し方上限のないコンテナを洗い出し、ホストのメモリとスワップの設計を見直す

4つ目は見落としやすい型です。Dockerのドキュメントは、OOMになるとカーネルは任意のプロセスを殺せると説明しています。Dockerデーモンも例外ではありません。上限のないコンテナが複数あるホストでは、コンテナ側に原因がないのに落ちる、ということが起こりえます。

Claude Code自身のプロセスがOOMで殺されたのなら、見るのはコンテナの上限ではなく本体のメモリ使用量です。その切り分けはClaude Codeのメモリリークで使用量が急増しOOM Killされる原因と対処にあります。コンテナ内のBashが拒否されて調査自体が始まらないときは、Claude CodeがDocker DesktopのコンテナでBashを拒否する原因と回避策が先です。

まとめ

137はSIGKILLの印にすぎないので、OOMKilled のtrueとfalseで最初に分けます。trueなら dmesg のkill行で時刻とプロセス名を合わせ、使用量の推移が横ばいなら上限不足、右肩上がりならリークと読みます。Claude Codeには読み取り系のコマンドだけを許可し、数字を表にして返させると、判断の根拠が手元に残ります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/permissions
  2. docs.docker.com/engine/containers/resource_constraints
  3. docs.docker.com/reference/cli/docker/container/stats
  4. docs.docker.com/reference/cli/docker/container/stop
  5. kernel.org/doc/html/latest/admin-guide/cgroup-v2.html
この記事を共有:XはてブLinkedIn