Claude CodeがCentOS 7でCPUを使い続ける原因とDockerでの切り分け
CentOS 7(カーネル3.10)ホストのDockerでself-hosted-runnerを動かすと、セッションが待機中もCPUを約2.3コア使い続けます。報告されている原因と、手元で確かめる手順、現実的な逃げ道をまとめます。
古いLinuxホストの上のDockerコンテナでClaude Codeのセルフホストセッションを動かしていて、待機中でもCPUが2コア分ほど張り付くなら、ホストのカーネルが3.10系かどうかを最初に見ます。GitHubのissue #99797には、CentOS 7(カーネル3.10)ホストで再現した報告があります。セッションは動き続けますが、CPUだけが無駄に使われます。
この記事は、その症状の見え方、報告されている原因の連鎖、手元で切り分けるコマンド、そして現実的な逃げ道を順に扱います。issueは2026-10-05に立てられたもので、回避用の設定は示されていません。最新の状況はissue #99797のページで確かめられます。
どんな症状が出るのか
報告された環境は、Claude Code 2.1.289のネイティブインストーラー版を、claude self-hosted-runner(--capacity 1)でDockerコンテナの中から動かす構成です。コンテナはUbuntu 22.04で、rootではないユーザーで動きます。ホストはCentOS 7でカーネルは 3.10.0-514.6.1.el7.x86_64、Dockerは23.0.1です。
セッションのプロセス(claude --print --sdk-url … で起動される子プロセス)に出た数字は次のとおりです。
| 項目 | 報告された値 |
|---|---|
| プロセス全体のCPU使用率 | 報告された値約225% |
| メインスレッド | 報告された値100%(22分11秒の稼働で21分50秒のCPU時間) |
Bun Pool スレッド | 報告された値8本が各13%前後 |
| 自発的コンテキストスイッチ | 報告された値22分間で約55万6千回 |
親の claude self-hosted-runner | 報告された値0.2% |
CPUを食うのはセッションの子プロセスだけで、親のRunnerは静かです。セッションの応答自体は正常に返るため、使い心地では気づきにくく、ホストの負荷やクラウドの請求で初めて見つかります。
straceで見えるのは「/」の無限ループ
報告者は strace -f -c を5秒間かけています。呼び出し回数が多い順に、futex が約24.8万回、write が約4.96万回(すべてイベントループのeventfd宛て)、epoll_pwait が約2.86万回です。openat・readlink・newfstatat・close はいずれも約2.48万回で、readlink はすべて失敗しています。
プールスレッドが繰り返しているのは、次の5手順です。
openat(AT_FDCWD, "/", O_RDONLY|O_CLOEXEC|O_PATH) = 13
readlink("/proc/self/fd/13", ..., 4096) = -1 EACCES (Permission denied)
close(13)
write(5, "\1\0\0\0\0\0\0\0", 8) # イベントループを起こす
newfstatat(AT_FDCWD, "/", {st_mode=S_IFDIR|0755, ...}, 0) = 0realpath("/") が毎秒およそ9,000回、しかも常に / に対して呼ばれています。パスを解決したいのではなく、失敗した同じ問い合わせが止まらず回っている形です。
原因は3つの条件が重なること
報告者の分析では、原因は次の連鎖です。
CPUが回り続けるまでの流れ
- 1
セッションのプロセスが自分を非dumpableにする
非dumpableの間、
/proc/<pid>/fdの所有者はroot:root、権限は0500になります。同じコンテナ内の親Runnerなど他のプロセスのエントリは、セッションのユーザーが所有しています。 - 2
カーネル3.10では、メインスレッド以外が自分のfdを読めない
この環境のカーネルでは、非dumpableなプロセスが自分の
/proc/self/fdを読めるのはメインスレッドだけです。同じプロセスの別スレッドはEACCESになります。 - 3
Bunの非同期realpathがスレッドプールで失敗する
Bunの非同期
realpathはスレッドプール上で、open(O_PATH)とreadlink("/proc/self/fd/N")の組でパスを解決します。そのため、非dumpableの間は必ずEACCESで失敗します。同期版はメインスレッドで動くので成功します。 - 4
失敗した呼び出しが、上限も待ちもなく再試行される
Claude Code側のどこかが、失敗した解決を制限なしで繰り返します。報告者もこの呼び出し元は特定できておらず、「何かが再試行している」と書いています。
なお、Claude Codeのネイティブ版が内蔵するBunは1.4.3です。報告者はBun単体でも再現しており、使ったのは公開済みの最新版1.4.2です。そのため、Bunの非同期 realpath が失敗したときの代替経路(パスを要素ごとにたどる方法)は、Bun側の課題としても挙げられています。
4つ目の呼び出し元は未特定で、1〜3つ目もカーネル3.10での観察です。報告者は、より新しいカーネルでは試していません。新しいカーネルで同じ条件が成立するかどうかは、issueからは分かりません。
手元のホストで確かめる手順
同じ症状かどうかは、Claude Codeのセッションを使わずに確かめられます。順番に3段階です。
1. カーネルとCPUの張り付きを見る
ホスト側で次を実行します。
uname -r
top -H -p "$(pgrep -f 'claude --print' | head -1)"uname -r が 3.10.0-…el7 の形なら条件に当たります。top -H はスレッド単位の表示で、メインスレッドが100%近く、Bun Pool が複数本とも動いていれば報告と同じ見え方です。
2. fdディレクトリの所有者を見る
コンテナの中で、セッションのプロセスの fd ディレクトリを確認します。
pid="$(pgrep -f 'claude --print' | head -1)"
ls -ld /proc/"$pid"/fd
grep ctxt /proc/"$pid"/statusroot:root で権限が dr-x------ なら、非dumpable化と一致します。2行目は、自発的・非自発的のコンテキストスイッチの累計です。数秒空けて2回測り、増え方が桁違いに大きければループの状況証拠になります。
3. カーネルの挙動だけを再現する
issueには、Bunもセッションも使わずにカーネルの挙動だけを見るPythonの再現コードが載っています。コンテナ内で、セッションと同じ非rootユーザーとして動かします。
python3 - <<'EOF'
import ctypes, os, threading
ctypes.CDLL(None).prctl(4, 0, 0, 0, 0) # PR_SET_DUMPABLE = 0
def t(where):
fd = os.open("/", os.O_RDONLY)
try: print(where, os.readlink(f"/proc/self/fd/{fd}"))
except OSError as e: print(where, e)
t("main ")
threading.Thread(target=t, args=("thread",)).start()
EOF報告では、メインスレッドは / を返し、別スレッドは [Errno 13] Permission denied になります。手元でも2行目が Permission denied になれば、カーネルがこの挙動をする環境です。新しいカーネルのホストで同じコードを動かして2行とも / が返れば、少なくともカーネル側の条件は外れています。
逃げ道はどこにあるか
報告者が採らなかった2案
issueは、試せなかった方法を2つ挙げています。
CAP_DAC_READ_SEARCHを付ける: スレッドが/proc/self/fdをたどれるようになります。ただしopen_by_handle_atも使えるようになり、コンテナ脱出の既知の経路になるため、報告者は付与しないと判断しています- ホストのカーネルを上げる: 共有のHPCノードでOSの入れ替えと再起動が要るため、当面は無理だという事情です
報告者は「セッションのプロセスをdumpableに保つ設定」や「この解決を避ける設定」があれば足りる、と尋ねています。issueの本文には、こうした設定は示されていません。
設定や運用で被害を抑える
原因そのものは外せなくても、CPUの無駄を上限で抑える手はあります。
| 手段 | 効くところ | 注意 |
|---|---|---|
Dockerの --cpus やKubernetesのCPU limit | 効くところ1コンテナが使える上限を固定する | 注意上限に達したコンテナはカーネルに絞られるが、プロセスは止まらずセッションは続く |
--kill-session-after-min | 効くところ暴走セッションの寿命に上限をかける | 注意上限に達したセッションは猶予の後に終了する(v2.1.260以降は猶予が既定15分) |
--release-idle-session-min | 効くところアイドルが続いたセッションのスロットを解放する | 注意解放したセッションがCPUを使い続けるかどうかは、docsに記載がない |
--capacity 1 と使い捨てコンテナ | 効くところセッションごとにコンテナを作り直し、張り付きを持ち越さない | 注意Runnerの強化指針に沿った構成でもある |
セルフホスト環境のdocsでは、1セッションあたりの出発点を「CPU request 2・limit 4」としています。待機中だけで約2.3コアを使うセッションは、この想定の半分以上を何もせずに消費します。ビルドやテストの余裕が、その分だけ減ります。
--capacity を2以上にしているなら、同じ症状は1セッションごとに出ると見るのが素直です。報告の計測は1セッションだけなので、本当に掛け算になるかは手元で測るまで分かりません。
ホストを替えるのが確実
Claude Codeの動作環境として挙がっているLinuxは、Ubuntu 20.04以降・Debian 10以降・Alpine Linux 3.19以降です。CentOS 7は一覧にありません。システム要件の確認はClaude Codeのシステム要件にまとめています。
カーネルを上げられないなら、Runnerだけを対応OSの別ホストに移し、CentOS 7側はGitホストやデータ置き場として残す分担が考えられます。Runnerの設定そのものはself-hosted-runnerの導入手順の内容がそのまま使えます。
似た症状との見分け
コンテナの中でClaude Codeがうまく動かない原因は、他にもあります。入口になるメッセージで分けられます。
bwrap: Can't mount proc … Operation not permittedが出て、sandbox内のコマンドが失敗する → 非特権Dockerの権限の問題で、enableWeakerNestedSandboxとbwrapPathが扱う症状です- エラーは出ず、セッションも動くが、CPUだけが張り付く → この記事の症状です。
readlink("/proc/self/fd/…")がEACCESで並ぶかをstraceで確かめます - コンテナの起動や認証が通らない → Dockerでの実行手順を扱うClaude Code Docker実行ガイドで、認証の持ち越しとヘッドレス実行を先に見ます
straceでは readlink が成功しているのにCPUが高いなら、別の原因です。この記事の症状とは切り分けます。
報告に添えると役立つ情報
issueが has repro ラベル付きでopenのままなので、手元でも再現したなら、同じissueに環境を足す価値があります。載せる項目は次のとおりです。
uname -rの出力と、ホストのディストリビューション名claude --version(報告はv2.1.289)- コンテナのユーザーと、付与したcapability・
no-new-privilegesの有無 - 上のPythonスニペットの出力
- 5秒間の
strace -f -cの上位行
報告者の環境は --cap-drop ALL に CHOWN・SETUID・SETGID だけを戻し、--security-opt no-new-privileges とDockerの既定seccompを使う構成でした。capabilityの付け方によって結果が変わるかは、issueには書かれていません。ここを自分の環境で変えて試した結果は、切り分けの材料になります。
まとめ
症状の核は、非dumpableなプロセスのスレッドが /proc/self/fd を読めないカーネルで、非同期の realpath("/") が失敗し続けることです。追えているのはカーネル3.10のCentOS 7という1つの環境で、原因の呼び出し元は未特定のままです。
古いホストでセルフホストRunnerを回す予定があるなら、導入前に上のPythonスニペットを1回動かしておくと、後から高いCPU使用率に気づく事態を避けられます。別スレッドの行でも / が返るホストなら、この症状の条件から外れます。