
【デジスマ ブログリレー 2日目】
デジスマチームの田口です。
最近 apple/container という Apple 純正の Linux コンテナサポートツールが v1 として正式リリースされました。軽量 VM ベースということで興味があり、これを使って何か面白いものを作れないかと色々触っていました。
ちょうど普段 Claude Code を claude --worktree で複数ブランチ並行で動かしていて、ブランチごとのローカルサーバの取り回しが地味な課題として残っていたので、apple/container の特徴を活かして解決できないかと考えて作ったのが、worktree(ブランチ)ごとにブランチ名のドメインを持つローカルサーバー環境を生やすツール branchbox です。本記事ではこのツールを紹介します。
作った動機
apple/container についてよく知らなかったので、調べてみたところ Docker Desktop と比べた時に次のような特徴があることが分かりました。
- コンテナ 1 つごとに軽量 VM を立てる(共有 VM 内の namespace 分離ではない)
- コンテナごとに専用 IP が割り当たり、ローカル DNS で名前解決できる
- macOS 26 (Tahoe) 以降必須
Docker Desktop と異なりコンテナごとに VM を立てるので使い捨て環境として使うのも面白いと思いましたが、今回は特に後者の「IP を細かく分けられる」性質に注目しました。Docker Desktop 系で同じことをやろうとすると、複数ローカルサーバを立てる時はポート公開 + ポート番号で分けるか、reverse proxy を挟むか、といった構成になります。一方で apple/container はコンテナごとに専用 IP を持てるので、「ブランチごとに別 IP」「ブランチごとに別ドメイン」が素直に実現できそうです。
これを claude --worktree の並行開発と組み合わせれば、worktree ごとに別 URL のローカルサーバを立てられて、URL からどのブランチかが直感的にわかります。ちょうどそういうローカルサーバの取り回しが普段から地味に課題だったので、ツール化することにしました。
具体的には、次のようなブランチに対して、次のようなドメインでローカルサーバーを起動します。
feat-login -> http://feat-login.internal:3000 fix-cart -> http://fix-cart.internal:3000 # 同じ 3000 でも衝突しない(専用IP)
ブランチ名がそのままドメインになるので、Claude に複数の worktree で並行開発を依頼している時でも、どの URL がどのブランチかが直感的にわかります。同じ 3000 番ポートが複数ローカルサーバで同時に開いていても、専用 IP のおかげで衝突しません。
設計の方針
設計の方針として、次の 4 つを置きました。なるべくプロダクトに手を加えずとも使えるよう、ツール側で完結できるようにしました。
- ブランチ名 = ドメイン(専用 IP + ローカル DNS、ポート公開・衝突なし)
- プロダクト非侵襲(対象リポジトリに
Dockerfileもcontainerの記述も不要) - Docker 前提に依存しない(コンテナ化の有無は無関係)
- ホットリロード対応
ちなみに、似たコンセプトのツールとして Vercel Labs の portless があります。こちらはプロキシベースで、host で動く dev server を https://myapp.localhost のような名前付き URL にマッピングしてくれるツールです。git worktree にも対応していて、fix-ui ブランチなら https://fix-ui.myapp.localhost で開けます。アプローチとしては「URL とポートを抽象化する」レイヤで、アプリはあくまで host で動かす形になります。
これに対して今回のツールは「アプリごと別 VM に隔離する」方向に倒し、ポートやドメインだけでなく node_modules・プロセス・(将来的には)DB まで含めた強い分離が手に入ることを期待しました。
実装ポイント
ここからは branchbox の主要な実装ポイントを紹介します。
host 側とコンテナ側の 2 か所の DNS 設定
*.internal *1の名前解決を有効にするには、host 側とコンテナ側の 2 か所の設定が必要です。
container system dns create internalは、host 側の/etc/resolverに*.internalを apple/container の DNS(127.0.0.1:2053)へ向ける設定を作るだけ- コンテナの登録先は「デフォルトドメイン」に基づくので、
~/.config/container/config.tomlの[dns] domain = "internal"で指定する必要がある
host 側の経路設定とコンテナ側の登録先が別管理になっているので、片方だけだと名前が解決されない点に注意が必要です。branchbox では URL 用のドメインを config.toml のデフォルトドメインから自動参照するようにして、設定の取りこぼしを防いでいます。
node_modules 用 volume の初期化判定
node_modules は host と共有せず、コンテナ専用 volume に分離しています。これにより host (macOS) と container (linux/arm64) でネイティブモジュールの ABI が食い違う、というコンテナ系開発でおなじみの問題を避けています。host 側にはマウントポイントだけが残ります。
VM 境界をまたぐファイル監視
apple/container はコンテナごとに軽量 VM を立てる方式なので、host の編集イベント(fsevents)が VM 内の watcher にそのままは届きません。素のままだとホットリロードが効かないケースがあるので、CHOKIDAR_USEPOLLING などのポーリング系の環境変数を branchbox 側で注入することで、ファイル変更が検知されるようにしています。
dev server は 0.0.0.0 bind が必要
127.0.0.1 で listen していると、コンテナ専用 IP 経由のアクセスが届かないので、HOST=0.0.0.0 を環境変数として注入しています。
ただし Vite 等は dev script 側に --host を渡す必要があるなど、フレームワーク側の事情が残るケースもあり、未検証ですがその場合はユーザー側でも少し気をつける必要があるかもしれません。
今後の展望
ここまででローカルサーバを立てる最低限はできました。今後機能を拡充するとしたら、次のようなものを試してみたいと思います。
- ミニスタック化: worktree ごとに専用ネットワークを切って、
web+db(+redis) を別コンテナで動かす。「ブランチ A のマイグレーションがブランチ B を壊さない」完全分離。(apple/container にはまだ docker compose 相当の機能がないので、今後の機能拡充に期待しています。) - 多言語ツールチェーン:
mise等で.tool-versions/.nvmrcを読んで、ブランチごとに別ランタイムにする(Node 18 と 22 を host で切り替える地獄を解消)。また対応言語も限られているので、拡充できると良さそうです。
おわりに
Claude Code の worktree 並行開発を快適にするためのローカルサーバ環境ツール branchbox の話を紹介しました。apple/container の per-container IP + ローカル DNS という特徴を活かすと、「ブランチ名 = ドメイン」という直感的なローカルサーバが割と簡単に作れました。
今回作成した branchbox は ken-tunc/branchbox に置いてあります。興味があれば触ってみてください。
We are hiring!!
エムスリーではエンジニアを絶賛募集中です! デジスマ診療や、他にも数多くの医療に関するプロダクトを開発しています。エムスリーでのプロダクト開発に興味がある方、ぜひカジュアル面談にご応募ください!
エンジニア採用ページはこちら
*1:今回はツールで発行する TLD として、ICANN が 2024 年に内部利用専用として正式予約した .internal を採用しました。