apple/container で worktree 並行開発を快適にするツールを作ってみた - エムスリーテックブログ

エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

apple/container で worktree 並行開発を快適にするツールを作ってみた

先月マルタ共和国へ旅行に行った時の風景です。本記事とは無関係です。

【デジスマ ブログリレー 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 つを置きました。なるべくプロダクトに手を加えずとも使えるよう、ツール側で完結できるようにしました。

  1. ブランチ名 = ドメイン(専用 IP + ローカル DNS、ポート公開・衝突なし)
  2. プロダクト非侵襲(対象リポジトリに Dockerfilecontainer の記述も不要)
  3. Docker 前提に依存しない(コンテナ化の有無は無関係)
  4. ホットリロード対応

ちなみに、似たコンセプトのツールとして 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!!

エムスリーではエンジニアを絶賛募集中です! デジスマ診療や、他にも数多くの医療に関するプロダクトを開発しています。エムスリーでのプロダクト開発に興味がある方、ぜひカジュアル面談にご応募ください!

エンジニア採用ページはこちら

jobs.m3.com

*1:今回はツールで発行する TLD として、ICANN が 2024 年に内部利用専用として正式予約した .internal を採用しました。