Claude Code の開発環境をつくる:WSL から Docker へ
WSL の systemd 問題で何度も環境が壊れた末に、Docker コンテナで開発環境を構築した話
← 前の記事: タイ旅行から始まった:やってみよう、でサイトを作るClaude Code を Linux で使う
Claude Code というのはターミナルで動く AI アシスタントだ。コードを読んで、書いて、修正して、テストも実行できる。私はこれを Linux 環境で使うことにした。
Linux で使うなら、まず「Linux 環境をどう調達するか」という問題がある。
最初に選んだのが WSL だった。WSL は Windows Subsystem for Linux の略で、Windows の中に Linux 環境を作れる仕組みだ。Windows のデスクトップはそのまま使いながら、ターミナルを開くと Linux が動いている——そういう二重構造になっている。インストールは簡単で、Microsoftが公式に出しているものだから信頼もしやすかった。
WSL の Ubuntu に Claude Code をインストールして、最初はそれで作業を始めた。動いた。問題なく動いた。
しばらくは。
systemd が壊れた
ある日、Claude Code を動かそうとしたら、何かが動かなかった。
原因を探ると、snap というパッケージ管理ツールが壊れていた。そして snap が壊れるとき、systemd も一緒に壊れていた。
systemd というのは Linux の「管理人」みたいな仕組みだ。バックグラウンドで動くサービス——起動、停止、監視——をまとめて取り仕切っている。これが壊れると、Linux の中のいろんな仕組みが動かなくなる。snap の壊れ方が systemd を巻き込む種類のものだった。
環境を再構築した。動いた。しばらく後、また壊れた。
また直した。また壊れた。
3回か4回か繰り返したとき、さすがに「これは環境自体に問題がある」と気づいた。直しても直しても壊れる、という状況は、もう「使い続けるべき環境」ではない。
最終的に、systemd を無効化することにした。管理人なしで Linux を動かす、ということだ。
これでsnap の問題は起きなくなった。起きなくなったが、今度は別の問題が出た。
Claude Code の実行が、体感で数倍遅くなった。
原因はわからなかった。Claude Code が systemd に何か依存しているらしいが、それが何かを調べる気力より先に、「この環境で作業を続ける気にはなれない」という気持ちが勝った。
遅い環境で働くのは、精神的に消耗する。ただでさえ AI ツールを使いながら試行錯誤しているのに、毎回ワンテンポ遅れる環境では、集中が保たない。
Docker に引っ越すことにした
WSL が信用できなくなった。別の方法で Linux 環境を作ることにした。
選んだのが Docker だ。Docker は「コンテナ」と呼ばれる仮想環境を作る仕組みで、Linux サーバーのような世界では昔から使われている。
コンテナというのを説明するのが少し難しいのだが、VM(仮想マシン)と比較すると伝わりやすい。VM は「PC の中に別の PC を丸ごと作る」イメージだ。OS ごとエミュレートするので重い。コンテナはそれより薄い。OS 全体を作り直すのではなく、Linux カーネル上で「隔離されたプロセスの塊」として動く。軽くて速い。
WSL との違いで一番大きかったのは、systemd がないことだ。
コンテナには管理人がいない。代わりに、必要なプロセスだけを直接起動する。管理人がいないから、管理人が壊れることもない。WSL で私を悩ませた問題が、そもそも発生しない構造になっている。
もう一つ、精神的に楽だったのが「壊れても作り直せる」という設計だ。
WSL の環境が壊れたとき、直す方法は「手動で調べて直す」しかなかった。何が壊れたのかを調べて、コマンドを調べて、一つずつ修正していく。これが何度も続いた。
Docker は違う。Dockerfile という設計図に「どういう環境を作るか」を書いておけば、壊れたらもう一度そこから作り直すだけだ。手動修復なしに、同じ環境が復元できる。壊れることを前提にして、壊れたら作り直す——という発想の転換だった。
これを冪等性(べきとうせい)という言葉で表すことがある。何度実行しても同じ結果になる、という性質だ。「昨日は動いたのに今日は動かない」が起きにくくなる。
一つだけ気をつけることがある。コンテナの中のファイルは、コンテナを消すと消える。プロジェクトのコードや設定ファイルは、コンテナの外(ホスト側)に置いておいて、コンテナからアクセスする形にした。こうすれば、コンテナを壊しても、コードは残る。
GPU が使えると知ったとき
Docker に引っ越す前、一つだけ心配していたことがあった。
私のマシンには NVIDIA の RTX 3090 が積んである。GPU だ。AI の画像生成や音声認識には GPU が必要で、将来的にそういうツールも使いたかった。
「Docker コンテナに GPU は使えるのか?」
調べたら、使えた。NVIDIA Container Toolkit という仕組みで、ホストマシンの GPU をコンテナ内から直接使える。コンテナの中から nvidia-smi というコマンドを打つと、RTX 3090 がちゃんと認識されている。
Docker は「軽量で制約がある」というイメージがあった。GPU まで使えるとは思っていなかった。
これが確認できたことで、コンテナ移行に踏み切る気持ちが固まった。Claude Code が動くだけでなく、画像生成も音声認識も、全部この環境に乗せられる。
実際、後から ComfyUI(画像生成)も faster-whisper(音声認識)もこのコンテナの中で動かすようになった。最初に「GPU が使える」とわかっていなければ、途中で「やっぱり別の環境が必要だった」となっていたかもしれない。
スマホから自宅のコンテナに入る
開発環境がコンテナになってから、もう一つ面白い使い方ができるようになった。
外出先からアクセスできる仕組みだ。組み合わせは3つ。
SSH は、ネットワーク越しにターミナルを操作する仕組みだ。自宅の PC で動いているコンテナに、外出先のスマホからログインできる。
tmux は、ターミナルの「セッション」を維持する仕組みだ。SSH で接続して Claude Code を動かしていると、通信が切れたとき作業ごと消える。tmux を使っていれば、セッションが保持される。再接続すれば、続きからやれる。
Tailscale は、VPN——仮想の専用ネットワーク——を簡単に作れるサービスだ。自宅の PC と外出先のスマホを同じネットワークに入れることで、外から SSH でつながる。
この3つを組み合わせると、移動中にスマホを開いて自宅のコンテナに入り、Claude Code に「これやっておいて」と指示を出して、画面を閉じて電車に乗る——ということができる。帰宅したら結果が出ている。
自宅の PC の前に座っていなくても、作業が進む。この感覚は最初、少し不思議だった。
ゾンビプロセスが 9万9千体
環境が整って快適に使い始めたが、一つ厄介なバグがあった。
Claude Code はコマンドを実行するたびに bash -c "..." で子プロセスを生む。問題は、この子プロセスが終了したあとの後始末だ。通常、親プロセスが wait() を呼んで子プロセスの終了を回収する。これをしないと、子プロセスは「ゾンビ」として残り続ける。プロセスとしてはもう動いていないが、プロセステーブル上に居座り続ける——幽霊のような存在だ。
40時間ほど連続で作業したとき、異変に気づいた。git が動かない。kill も動かない。コマンドを打つたびに unknown error number 11(EAGAIN)が返ってくる。
プロセス数を数えてみた。
99,030。うち 98,039 が bash のゾンビだった。
メモリは 20GB 以上空いていた。メモリの問題ではない。プロセステーブルが埋まって、新しいプロセスを生めなくなっていた。最終的に Claude Code は SIGABRT で死んだ。
原因は明確だ。Claude Code が生んだ bash の子プロセスを、誰も回収していなかった。1回のコマンド実行で1体のゾンビが生まれ、40時間でほぼ10万体に達した。
対策は Docker の --init フラグだ。 docker run --init をつけると、コンテナの PID 1 に tini という軽量の init プロセスが入る。init プロセスの役割は、親を失った子プロセスを自動で回収すること。ゾンビが生まれても、tini が代わりに wait() を呼んでくれる。
docker-compose.yml なら init: true を1行追加するだけだ。
services:
dev:
init: true
# ...これを入れてからは、ゾンビが溜まらなくなった。Claude Code 側のバグが直るまでの対処だが、効果は確実だ。なお、この問題は Anthropic にバグレポートとして報告済みだ。
ただ、ここで Docker の「壊れても作り直せる」性質も活きた。最悪の場合、コンテナを再起動すれば全部きれいになる。WSL のときだったら OS ごと再起動が必要だったが、コンテナなら数秒で済む。「壊れても安心」な環境だから、多少のバグは許容できる。
dev サーバーが壊れる
もう一つ、地味だが厄介な問題があった。
Next.js で開発するとき、next dev で開発サーバーを立ち上げておくと、コードの変更が即座にブラウザに反映される。普通はこの dev サーバーを動かしっぱなしにして作業する。
ところが Claude Code は、コードを変更するたびに pnpm run build を実行して確認しようとする。next build が走ると .next/ ディレクトリを丸ごと作り直す。このディレクトリは next dev も使っている。結果、dev サーバーが参照しているファイルがビルドの途中で消え、ENOENT(ファイルが見つからない)でクラッシュする。
最初に試した対策は、ビルドの出力先を分けることだった。next.config.ts で distDir を環境変数で切り替えられるようにして、dev は .next/、build は .next-build/ を使うようにした。理屈上はこれで衝突しないはずだった。
だめだった。ビルド中のモジュール解決やファイル監視が干渉するのか、dev サーバーはやはり壊れた。
結局たどり着いた運用は、dev 中は full build を避けることだった。型チェックと lint だけなら .next/ に触らない。
npx tsc --noEmit && pnpm run lintこれで dev サーバーは壊れない。full build はコミット前に dev を止めてから実行する。完璧な解決ではないが、現実的な妥協だ。AI と一緒に開発していると、こういう「AI の動き方と既存ツールの相性」に工夫が必要になる場面がある。
壊れても安心な環境
WSL で何度も同じ壊れ方をして、そのたびに同じ修復作業をして——それに疲れて Docker に引っ越した。
結果的に、壊れても数秒で再構築できる環境が手に入った。
systemd がないから systemd が壊れない。Dockerfile があるから環境を再構築できる。コンテナを再起動すればプロセスの問題がリセットされる。壊れることを前提にした設計が、逆に安心感を生んでいる。
この Docker コンテナの上に、次回以降で紹介する Ralph Wiggum Loop も、ComfyUI も、GPTs 開発も、全部乗っている。このシリーズのこの先に出てくる話は、すべてこのコンテナの中で起きている。
あとで知ったのだが、Windows の Docker Desktop は内部的に WSL2 の上で動いている。WSL から逃げたつもりが、その下にはまだ WSL がいた。結局逃げ切れてはいなかったのだが、直接 WSL を触らなくなったことで問題が起きなくなった。レイヤーが一枚挟まるだけで、体験はまったく違う。
WSL から Docker への引っ越しは、「環境が壊れる問題を解決した」というより、「壊れても困らない構造に変えた」という話だ。直すのをやめて、作り直しを選んだ。それだけで、ずいぶん気持ちが楽になった。