git worktreeを活用したLLMコーディングエージェントによる並行開発の話が話題になっています。git worktree自体は以前から使っていましたが、複数のworktreeで同時に開発を進めるには一筋縄ではいかないことは薄々わかっており、なかなか本格的に手を出せずにいた状況です。並行開発を手助けするツールが登場してきたこともあり、このタイミングで本腰を入れて取り組んでみることにしました。
具体的には、ポート競合やDB競合、worktreeの管理など、考慮すべき点は多岐にわたります。各個人で試行錯誤し、現状自分はこう開発しているといった内容の記事や並行開発に利用できるツールの紹介の記事は多いです。一方、課題ごとに選択肢を並べて、どう選ぶかの判断材料を整理した記事はあまり見かけない印象があります。試行錯誤する中で課題と解決策が整理できてきたので、その記録を残す目的で本記事を書くことにしました。
自分のユースケースとしては、以下のような環境を想定しています。マイクロサービス構成など、プロジェクトの構造によっても課題や解決策は変わりますが、今回はモノレポ構成のWeb開発のケースを想定して話を進めます。
また、git worktreeについての基本的な知識はある前提で話を進めます。worktreeの基本的な概要や使い方は扱いません。
並行開発の環境構築には、大きく分けて2つの方向性があります。
本記事では主に前者の課題と解決策を詳しく扱い、devcontainerによるアプローチについては軽く触れます。
ここからは必要なものだけ分けるアプローチを取る場合の課題と解決策について、具体的な例を交えながら紹介していきます。
複数のworktreeで同じWebアプリケーションを起動すると、同一ポートを使おうとして競合が発生します。フロントエンド・バックエンド・BFFなど複数のサービスがある構成では、この問題は掛け算で増えていきます。
具体例: ハッシュベースのポート自動割り当て
worktree作成後のセットアップスクリプトとして、ブランチ名からポートを決定的に算出し.envに書き込む方法があります。これにより、ブランチごとに毎回同じポートが割り当てられるため、「このブランチはいつも3847番」のように覚えやすくなります。
# !/bin/bash
# ブランチ名からポート番号を決定的に算出して.envに書き込む
branch=$(git branch --show-current)
hash=$(echo -n "$branch" | cksum | cut -d ' ' -f1)
port=$((hash % 10000 + 3000))
sed -i'' -e "s/^PORT=.*/PORT=$port/" .env 2>/dev/null \
|| echo "PORT=$port" >> .env
.envファイルにPORTが書き込まれたら、通常通り開発サーバーを起動します。(例: Viteの場合)
npm run dev --port $PORT
具体例: worktreeごとに固定のポートを割り当てる
ポートを環境変数や設定ファイルで変更可能にし、worktreeごとに固定のポートを割り当てる方法もあります。こちらもworktree作成後のセットアップスクリプトとして実行します。
# !/bin/bash
worktree=$(basename "$(git rev-parse --show-toplevel)")
case "$worktree" in
"worktree-a") port=3001 ;;
"worktree-b") port=3002 ;;
*) port=3000 ;;
esac
# .envにポートを書き込む
sed -i'' -e "s/^PORT=.*/PORT=$port/" .env 2>/dev/null \
|| echo "PORT=$port" >> .env
localhost:3000で保存したログイン情報がlocalhost:3847では候補に出てきません。こちらについてはworktree自体の使い回しで、worktreeごとにポートを決めておけば一度のログインでパスワードなどのログイン情報は保存できるため、使いまわせます。ポート競合が問題になるのは、主にフロントエンドからバックエンドへアクセスする必要がある場合です。フロントエンドがバックエンドのポートを知る必要があるため、両方を同時に起動すると競合します。一方、フロントエンド単体やバックエンド単体の修正であれば、フレームワークが自動で空きポートを割り当ててくれることも多く、あえてポートを固定する必要はありません。
たとえばViteを利用している場合、デフォルトのポート(5173)が使用中であれば自動的にインクリメントされた空きポートが割り当てられます。フロントエンドの修正が複数同時に走る場合でも、すでに起動している他のworktreeのバックエンドサーバーへアクセスすれば動作確認できるため、この方法で十分なことも多いです。
このアプローチは「並行でコードを書きつつ、動作確認は1つずつ行う」ワークフローに適しています。ただし、LLMにバックグラウンドで開発サーバーの起動を任せる場合など、どのworktreeのサーバーが起動中か把握しきれなくなることがあります。そうした場合に備えて、ポートを指定してプロセスを停止する手段を用意しておくと便利です。
lsof -i :3000 -t | xargs kill -9
nginxやcaddyなどのリバースプロキシを使い、サブドメインやパスで各worktreeのサーバーに振り分ける方法もあります。
feature-a.localhost → localhost:3001
feature-b.localhost → localhost:3002
サブドメインの違いではchromeのパスワードマネージャーも効くため、ポートを切り替える方法よりも開発体験が向上します。 しかし、並行開発のためだけにリバースプロキシを立てるのはやや大げさな気もします。実際に自分はこの方法を試せていません。
DBについて、同じDBを共有するか、worktreeごとに別のDBを立てるかの選択肢があります。スキーマ変更などのマイグレーションを含むタスクも並行で進める場合は、DBを分ける必要に迫られることも多いです。
スキーマ変更を伴わないタスク同士(例: フロントエンドの修正が2つ)だったり、スキーマ変更がある場合でも互いに干渉しないようにタスクの順序を工夫できる場合は、DBを共有する方法で十分なことが多いです。
具体例: shell scriptで.envにDB設定を書き込み、docker-composeで読み込む
この例では、ブランチ名から決定的にDBのポートを算出し、.envに書き込んでからdocker-composeで起動する方法を示しています。これにより、worktreeごとに独立したDBコンテナを立てることができます。
# !/bin/bash
worktree=$(basename "$(git rev-parse --show-toplevel)")
sanitized=$(echo "$worktree" | tr '/' '-' | tr '[:upper:]' '[:lower:]')
db_port=$(($(echo -n "db-$worktree" | cksum | cut -d ' ' -f1) % 10000 + 5432))
# .envにDB設定を書き込む
cat > .env <<EOF
COMPOSE_PROJECT_NAME=myapp-${sanitized}
DB_PORT=${db_port}
POSTGRES_DB=myapp_dev
POSTGRES_PASSWORD=password EOF
docker compose up -d
# docker-compose.yaml
services:
postgres:
image: postgres:16
ports:
- "${DB_PORT:-5432}:5432"
environment:
POSTGRES_DB: ${POSTGRES_DB:-myapp_dev}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password}
COMPOSE_PROJECT_NAME をworktreeごとに変えることで、コンテナ名が衝突せず複数のDBを同時に起動できます。
実際にworktreeごとにDBコンテナを個別に立てる構成を試しましたが、運用上の負担が大きいと感じました。スキーマ適用やseedの投入によりworktree作成時のセットアップが遅くなるうえ、複数リポジトリの開発を掛け持ちしているとDBコンテナが際限なく増えてディスクを圧迫します。不要になったコンテナのクリーンアップも必要で、管理の認知コストが高くなります。必要に迫られれば採用しますが、個人的には積極的に選ぶ方法ではありません。
具体例: worktree作成時にデータベースを作成し、アプリケーションの接続先を切り替える
メインワークツリーでcontainer_name: myapp-postgresを指定してDBコンテナを起動している前提です。
# !/bin/bash
worktree=$(basename "$(git rev-parse --show-toplevel)")
sanitized=$(echo "$worktree" | tr '/' '-' | tr '[:upper:]' '[:lower:]')
db_name="myapp_${sanitized//-/_}"
# 起動中のDBコンテナに新しいデータベースを作成
docker exec myapp-postgres psql -U postgres -c "CREATE DATABASE \"${db_name}\";" 2>/dev/null \
|| echo "Database ${db_name} already exists"
# .envに接続先を書き込む
sed -i'' -e "s|^DATABASE_URL=.*|DATABASE_URL=postgresql://postgres:password@localhost:5432/${db_name}|" .env 2>/dev/null \
|| echo "DATABASE_URL=postgresql://postgres:password@localhost:5432/${db_name}" >> .env
PostgreSQLのデータベース名を分けることで、コンテナを増やさずに論理的に分離する方法もあります。
git worktreeでは.git管理下のファイルは自動的に分離されますが、.gitignore対象のファイルは自分で管理する必要があります。
| 種別 | 例 | 特性 |
|---|---|---|
| 環境設定 | .env, .env.local |
機密情報を含む、軽量 |
| 依存パッケージ | node_modules/, vendor/, .husky |
大容量、再生成可能 |
| IDE設定 | .vscode/settings.json, .idea/ |
git管理していない個人設定 |
| submodule | libs/shared/ 等 |
git管理だがworktreeで別途対応が必要 |
ファイルの種別ごとに「共有する / 分離する」を判断する必要があります。すべてを一括で同じ方法にするのではなく、特性に応じた選択が必要です。
具体例: worktree作成時にgitignoreされているファイル・ディレクトリにシンボリックリンクを貼る
#!/bin/bash
# usage: ./setup-worktree.sh <worktree-path>
MAIN_WORKTREE=$(git worktree list | head -1 | awk '{print $1}')
TARGET=${1:?"worktree path is required"}
# .envはシンボリックリンクで共有
ln -sf "${MAIN_WORKTREE}/.env" "${TARGET}/.env"
# node_modulesもシンボリックリンクで共有
ln -sf "${MAIN_WORKTREE}/node_modules" "${TARGET}/node_modules"
メインのworktreeにあるファイルへシンボリックリンクを張る方法です。
.env等)の一元管理ができるnode_modules/を共有すると、依存パッケージの変更で競合する可能性がある具体例: worktree作成時にgitignoreされているファイル・ディレクトリをコピー・再インストールする
#!/bin/bash
# usage: ./setup-worktree.sh <worktree-path>
MAIN_WORKTREE=$(git worktree list | head -1 | awk '{print $1}')
TARGET=${1:?"worktree path is required"}
# .envはコピーして管理cp "${MAIN_WORKTREE}/.env"
"${TARGET}/.env"
# node_modulesは再インストール (npm ci など)
cd"${TARGET}"&& npm ci
node_modules/等の容量が大きい場合にディスクを圧迫するpnpmを利用している場合、グローバルストアを共有するためディスク消費を大幅に抑えられます。
git worktreeではsubmoduleが自動的には初期化されません。worktree作成後に明示的にgit submodule update --initを実行する必要があります。これもセットアップスクリプトに含めておくと漏れがないです。
huskyなどの初期化が必要なツールもあるため、worktree作成時のセットアップスクリプトで必要な初期化処理をまとめて行うと便利です。
ここまで述べてきた課題を根本的に回避するアプローチとして、devcontainerで環境を丸ごと隔離する方法があります。
devcontainerではdocker-composeと組み合わせて、アプリケーションコンテナ(フロント+バック)とDBコンテナを定義し、worktreeごとに独立した環境を構築できます。ポート・DB・ファイルの競合問題がそもそも発生しません。
正直なところ、自分自身はまだdevcontainerを並行開発で本格的に使えていません。セットアップコストの高さに敬遠してしまっている部分もあるので、今後試してみたいと思っています。
ここまでを踏まえ、現在運用している構成例を紹介します。自分の場合はworktreeをせいぜい5つ程度で運用しており、それに耐えられる構成を考えた結果、以下に落ち着きました。分離の選択肢を多く紹介しましたが、分離にはそれぞれセットアップや管理のコストが伴うため、基本は共有にしておき、必要に迫られたときだけ分離する方が運用コストを低く抑えられるという判断です。
ほとんどのタスクはこの構成で問題なく対応できています。
運用面では、セットアップスクリプトを用意し、上記の設定でworktreeの作成・初期化をまとめて行えるようにしています。このスクリプトはzshのエイリアスに登録してあり、どこからでも実行可能です。worktree自体はタスクごとに削除せず使い回しています。
修正内容に応じた柔軟な対応も行っています。たとえば、シンボリックリンクで共有している.envを一時的にコピーして書き換えたり、DBも必要に応じて新規データベースを作成して接続先を切り替えたりしています。
node_modulesの共有についても、ほとんどの場合問題になりません。依存関係は追加されることがあっても削除されることは稀だからです。依存関係の整理などで削除が必要になった場合は、そのworktreeでのみシンボリックリンクを外して個別にインストールすれば対応できます。
これらの運用は少し面倒に聞こえるかもしれませんが、その操作自体もLLMに任せることで、実際の運用上の負担はかなり軽減されます。セットアップスクリプトもLLMに管理させているため、必要なときに必要な操作をLLMに指示するだけで環境の切り替えや初期化が完了します。
※前提として、ポートやDB接続先などがすべて環境変数で管理されている必要があります。
普段はClaude Code for VS Codeの単一ウィンドウから、複数のworktreeを立てて並行で進めています。
cdして切り替えて実装してくれる。ここはSkill化している。cdして移動するか、VSCodeのアクティビティバーの「ワークツリー」を右クリックして「統合ターミナルで開く」からそのworktreeのターミナルを開く。npm(pnpm) run devする。この運用だと、ウィンドウの切り替えが最小限で済み、差分確認やターミナル操作も同一ウィンドウ内で完結するため、複数のworktreeを行き来しながらの開発がスムーズに行えます。
今回は、git worktreeを活用した並行開発における課題と解決策を整理しました。
自分自身、考慮すべき点の多さからなかなか踏み出せずにいましたが、実際にやってみると一番シンプルな「基本は共有」という構成で十分に運用できています。本記事で扱った内容は概念的なものが中心であり、実際にはプロジェクトの構成やチームの状況に応じた意思決定が必要です。まずはgit管理外の設定ファイルやセットアップスクリプトを用意して個人で試してみて、そこから徐々にプロジェクト全体の運用ルールへ広げていくのが現実的な進め方でしょう。実際にやってみると、記事だけでは見えてこない課題や工夫が見つかるはずです。