朝、リビングでラップトップを開き、出勤までの数分間で Claude Code に指示出しをする。家を出て電車に乗ったら、携帯を開いて、テストの結果を眺める。昼はカフェでラップトップを開き、優雅に手直しを入れる。夜は湯船に浸かりながら、携帯でステージングデプロイして、検証。ベッドでゴロゴロしながら Claude Code でバグ修正の依頼を投げて、寝る。
こんな日常に憧れますよね。ですが、開発環境はラップトップのなかで動いています。蓋を閉じれば、走らせていたテストもそこで止まります。携帯からは Claude Code on the Web で同じリポジトリを開けますが、そこで動くのは、ラップトップの Dev Container で揃えたツールが入った環境ではありません。
朝、通勤時間、カフェ、入浴中、そして寝る前。どれも細切れの時間で、まとまった時間は取れません。そこで半年ほど前から、趣味の開発環境のほとんどを DevPod に移しました。今では、ソースコードもツールも実行中のプロセスも、自宅サーバの中で動いています。
Claude Code on the Web では、手元の Dev Container が起動しない
自宅サーバを置く前に、Anthropic が用意している環境で足りるかどうかを確かめました。携帯から Claude Code を動かすだけなら、Claude Code on the Web があります。 GitHub のリポジトリと開始元のブランチを選べば、Claude Code が Anthropic の管理する隔離 VM でタスクを実行します。
しかし、その VM は筆者がラップトップで使っていた Dev Container ではありません。一般的な開発ツールは用意されており、setup script で依存関係や環境変数も設定できますが、リポジトリの Dev Container をそのまま起動する仕組みではありません。1
筆者が開きたいのは、携帯から開ける任意の環境ではなく、Node.js も fish も Chromium も入った普段の Dev Container そのものです。要件が固まったタスクを非同期に投げるなら Claude Code on the Web が便利ですが、進めている作業を環境ごと持ち出すには足りません。
自宅サーバに Talos Linux で Kubernetes を構築する
自宅サーバには、Talos Linux でシングルノード Kubernetes を構築しました。 Kubernetes と聞くと、自宅サーバごときに大げさに聞こえるかもしれませんが、開発環境ごとにコンテナをデプロイするには思いの外便利です。
使っているのは ASRock DeskMini 110 です。 2 TB の NVMe SSD を載せ、その翌月にメモリを 32 GB へ増設しました。 DeskMini 110 は SO-DIMM 二枚差しで 32 GB が上限です。この 32 GB という天井に後で苦しむのですが…まあそれは置いておいて。
OS は Talos Linux v1.12.4、Kubernetes は v1.35.0 です。一台しかないため、control plane と worker を兼用し、allowSchedulingOnControlPlanes: true を設定しています。
開発環境が載っているのは、この DeskMini だけです。壊れたときに、何をどこまで戻せばよいのか分からなくなると困ります。 Talos Linux を選んだのは、Kubernetes を含む OS 全体を宣言的に管理できるためです。ノードには SSH がなく、変更は machine config と talosctl を通して適用します。
Tailscale で、外出先でも talosctl を使う
DeskMini が家で動いていても、外から届かなければ意味がありません。自宅回線は transix の DS-Lite で、IPv4 のポートフォワーディングができません。これではカフェでラップトップを開いても、自宅の開発環境にアクセスできませんね。そこで、Talos Linux に Tailscale のシステム拡張を入れ、外出先から Kubernetes API と talosctl API へ接続しています。
ラップトップを tailnet に入れておけば、場所を問わず届きます。開発環境は自宅で動き続けるため、出かけるときに同期したり、コンテナを作り直したりする必要もありません。
devcontainer.json は一つにできず、Compose 用と Kubernetes 用に分けた
もともと使っていたのは、手元の Docker Compose で起動する Dev Container です。 Kubernetes の上でも同じ Dev Container を開くために、DevPod を足しました。
DevPod は、devcontainer.json を任意のバックエンドで動かす、クライアント側だけのツールです。どこで動かすかは provider で決まります。ローカルの Docker でも、クラウドの VM でも、この記事のように Kubernetes でも、同じコマンドで workspace を開けます。そのなかで Kubernetes を選んだのは、使わない workspace を止めておき、必要になったら同じ PersistentVolume から再開できるからです。
Compose 用は残しました。 DevPod で自宅の workspace を開くにはネットワークが要ります。繋がらない場所では、ラップトップの Docker Compose で開発したいからです。 Docker Compose も DevPod も、同じ .devcontainer の下に置いた設定を読んで起動します。
素直に考えると、devcontainer.json を一つにして両方から使い回したくなります。 DevPod は Dev Container の仕様に従うので、手元で使っていたファイルがそのまま読めるように見えます。
しかし残念ながら、DevPod の Kubernetes provider は Docker Compose を扱えません。 DevPod は compose を使う設定を見つけると driver が Docker かどうかを確かめ、違えば docker compose is not supported by this provider, please choose a different one と言って止まります。2 手元の devcontainer.json は dockerComposeFile でサービスを指しているので、この設定は Kubernetes provider では使えません。
そこで、.devcontainer/compose/ と .devcontainer/k8s/ に devcontainer.json を一つずつ置きました。次の表で違うのは、イメージの指定の書き方です。
| Compose 用 | Kubernetes 用 | |
|---|---|---|
| 書くキー | dockerComposeFile と service | build |
| 指す先 | compose.yaml の app サービス | .devcontainer/Dockerfile |
Compose 用が指す compose.yaml も、中では同じ .devcontainer/Dockerfile を同じコンテキストで build しています。できあがるイメージは、どちらも同じです。
forwardPorts の 5173、fish を既定にするターミナル設定、四つの VS Code 拡張、postCreateCommand の ./.devcontainer/post_create.sh は、文字列まで揃えてあります。どちらから開いても、目に入るものと使えるツールは同じです。
その Dockerfile は、mcr.microsoft.com/devcontainers/javascript-node:24-bookworm に fish、Chromium、Claude Code、端末のセッションを残す herdr などを足したものです。 Compose 用の compose.yaml はリポジトリ全体を /workspaces に bind mount し、Claude Code の認証情報と auto-memory は named volume に保存して、コンテナを作り直しても残します。
二つの devcontainer.json も compose.yaml も Dockerfile も公開しています。実物はこちらで読めます。
Talos の kubelet には、ホストのディレクトリが見えない
DevPod workspace のデータは、local-path-provisioner がホスト上のディレクトリに確保した PersistentVolume に保存します。ここで一度つまずきました。 Talos Linux の kubelet はコンテナとして動いて自分の mount namespace を持つため、local-path-provisioner が作ったホストのディレクトリが見えません。 DevPod はボリュームを subPath で切って渡します。サブディレクトリを探して割り当てるのは kubelet の仕事なので、パスが見えない kubelet はここで失敗します。3 machine config の extraMounts でそのディレクトリを kubelet へ渡し、propagation を rshared にすると、DevPod が workspace を起動します。
PersistentVolume とは別に、Chromium を動かすための設定も要ります。 DevPod 用の namespace では、Pod Security Standards の privileged を許可しています。開発用コンテナで Chromium を動かすために、capability と seccomp を緩めるからです。
devpod up 一つで、workspace が立つ
設定が揃えば、立ち上げはコマンド一つです。
devpod up github.com/yantene/yantene.net --devcontainer-id k8s --ide vscode
--devcontainer-id に .devcontainer/ の下のフォルダ名を渡すと、DevPod がそのフォルダの devcontainer.json を読みます。 DevPod がリポジトリを取ってきて、devcontainer.json からイメージを組み、workspace を作ります。最後に VS Code を繋ぎます。 Node.js のバージョンや VS Code 拡張、コマンドラインツールは、それぞれのリポジトリに置いた Dev Container の設定から入ります。
ターミナルだけで足りるなら、--ide none で立てて devpod ssh yantene-net で入ります。 yantene-net は、リポジトリ名 yantene.net のドットがハイフンに置き換わった workspace の名前です。
ラップトップを閉じても、Claude Code のセッションは動き続ける
flowchart LR
subgraph laptop["ラップトップ"]
term["Terminal"]
vscode["VS Code"]
end
subgraph phone["携帯"]
app["Claude アプリ"]
end
term --> devpod["DevPod"]
vscode --> devpod
devpod -->|"Tailscale 経由"| api["Kubernetes API"]
app <-->|"Remote Control"| relay["Claude のサーバー"]
subgraph home["自宅のシングルノード Kubernetes"]
api
subgraph workspace1["yantene.net workspace"]
claude["Claude Code"]
app1["ソースコード"]
tools1["Node.js、fish、Chromium など"]
end
subgraph workspace2["別のアプリケーションの workspace"]
other["ソースコードとツール"]
end
api --> workspace1
api --> workspace2
end
relay <--> claude
冒頭の五つの場面に戻ります。 workspace がラップトップから独立していると、その中で動く Claude Code もラップトップに依存しません。筆者は workspace の Claude Code で /rc を実行し、Remote Control を有効にしています。4
リビングとカフェでは、ラップトップから操作します。 VS Code は DevPod から開き、ターミナルからは devpod ssh yantene-net で入ります。
ターミナルを開いたら、何より先に herdr を立ち上げます。 tmux でもセッションを残せます。
ラップトップを閉じると、接続は切れます。 VS Code の統合ターミナルは、開き直したときの再読み込みでセッションが落ちます。どちらも、herdr の中で動かしていれば入り直して同じ画面に戻れます。
電車と風呂とベッドでは、ラップトップを閉じたまま、携帯の Claude アプリから同じセッションを操作します。携帯を tailnet に入れる必要はありませんし、SSH クライアントも VS Code も入っていません。 DevPod で workspace へ入るのはラップトップだけで、携帯が話す相手は workspace の中で動いている Claude Code だからです。
どちらから操作しても、Claude Code は自宅の workspace でファイルを編集し、テストを走らせ、Git を操作します。端末を替えても、Node.js のバージョン、fish の設定、編集中のファイル、実行中のテストはそのままです。これで、冒頭の一日が成立します。
workspace が DeskMini 110 に集まり、32 GB が上限になる
シングルノードに workspace を置くと、開発用の負荷と Kubernetes 自身の負荷が DeskMini 110 に集中します。 DevPod workspace がメモリを使い切り、containerd、kubelet、etcd、machined の health check がそろって 2 分間失敗したことがありました。 Pod を退避させる別のノードもありません。
Talos Linux の既定値では、システム予約メモリが 512 MiB、eviction の閾値が残り 100 MiB でした。 eviction は、ノードの空きが閾値を割ったときに kubelet が Pod を追い出す動作です。この設定では、kubelet が eviction を始めるより先に、Talos Linux の OOMController がプロセスを落とします。 OOMController が見ているのは空きの量ではなく、メモリ待ちで処理が止まっている割合です。空きが 100 MiB になるのを待ちません。
そこで、システム予約メモリを 2 GiB へ増やし、eviction の閾値を残り 1 GiB へ引き上げました。システム予約が 1.5 GiB、eviction の閾値が約 0.9 GiB 増えます。合わせて 2.4 GiB が Pod の取り分から引かれます。ノードの allocatable は、変更前が 30.44 GiB でした。搭載した 32 GB から、カーネルなどが先に押さえる分を引いた値です。ここから 2.4 GiB 減って 28.04 GiB になります。
eviction を早めるのは、閾値を 0.9 GiB 上げた分です。ノード全体が応答しなくなる前に、kubelet が Pod を追い出せます。
ただ、Claude Code にいくつもの作業を並列で任せようとすると、28.04 GiB ではどうしたって足りません。増設で埋められるのはここまでで、DeskMini 110 にこれ以上は挿さりません。
こうなってくるとメモリモリモリマシンが欲しくなってくるところですが、聞くところによると、AI 需要というやつのせいで、メモリが高くなっているそうです。
AI 需要、許せませんね。
Footnotes
-
Configure cloud environments に、クラウドセッションへ最初から入っているツールの一覧と、足りないものを入れるための setup script の書き方があります。リポジトリの Dev Container を読む仕組みは出てきません。 ↩
-
DevPod は Docker Compose を扱うとき driver が
DockerDriverかどうかを確かめ、そうでなければdocker compose is not supported by this provider, please choose a different oneを返します (pkg/devcontainer/compose.go)。 ↩ -
Talos Linux の kubelet は独立した mount namespace を持つため、
subPathやDirectoryOrCreateのようにホストのパスを kubelet 自身が読む機能ではextraMountsが必要になる、と開発者が説明しています。 ↩ -
Continue local sessions from any device with Remote Control に、
/remote-control(/rc) の使い方と、セッションが手元のマシンで動き続けるので実行もファイルもそこに留まることが書かれています。 ↩
