Fullstack TypeScriptのモノレポ開発におけるgit worktreeを利用した並行開発の課題と解決策

Fullstack TypeScriptのモノレポ開発におけるgit worktreeを利用した並行開発の課題と解決策

目次

    はじめに

    git worktreeを活用したLLMコーディングエージェントによる並行開発の話が話題になっています。git worktree自体は以前から使っていましたが、複数のworktreeで同時に開発を進めるには一筋縄ではいかないことは薄々わかっており、なかなか本格的に手を出せずにいた状況です。並行開発を手助けするツールが登場してきたこともあり、このタイミングで本腰を入れて取り組んでみることにしました。

    具体的には、ポート競合やDB競合、worktreeの管理など、考慮すべき点は多岐にわたります。各個人で試行錯誤し、現状自分はこう開発しているといった内容の記事や並行開発に利用できるツールの紹介の記事は多いです。一方、課題ごとに選択肢を並べて、どう選ぶかの判断材料を整理した記事はあまり見かけない印象があります。試行錯誤する中で課題と解決策が整理できてきたので、その記録を残す目的で本記事を書くことにしました。

    前提

    自分のユースケースとしては、以下のような環境を想定しています。マイクロサービス構成など、プロジェクトの構造によっても課題や解決策は変わりますが、今回はモノレポ構成のWeb開発のケースを想定して話を進めます。

    • LLMコーディングエージェント(Claude Code等)を使った並行開発を想定
    • Web開発(TypeScriptによるフロントエンド + バックエンド構成でnpm/pnpmを使用)
    • DBはローカルのDockerコンテナで構築

    また、git worktreeについての基本的な知識はある前提で話を進めます。worktreeの基本的な概要や使い方は扱いません。

    並行開発のアプローチ

    並行開発の環境構築には、大きく分けて2つの方向性があります。

    • 必要なものだけ分ける: ホスト上のgit worktreeを使い、ポート・DB・ファイルなどを課題ごとに「共有する / 分離する」を選んでいく
    • 環境をまるごと分ける: devcontainerなどを使って、worktreeごとにフロントエンド・バックエンド・DBを丸ごと独立した環境として構築する

    本記事では主に前者の課題と解決策を詳しく扱い、devcontainerによるアプローチについては軽く触れます。

    必要なものだけ分ける場合の課題と解決策

    ここからは必要なものだけ分けるアプローチを取る場合の課題と解決策について、具体的な例を交えながら紹介していきます。

    1. ポート競合

    複数のworktreeで同じWebアプリケーションを起動すると、同一ポートを使おうとして競合が発生します。フロントエンド・バックエンド・BFFなど複数のサービスがある構成では、この問題は掛け算で増えていきます。

    選択肢A: 各worktreeで異なるポートを利用する

    具体例: ハッシュベースのポート自動割り当て

    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
    • メリット
    1. 各worktreeで独立して動作確認ができる 動作確認を同時に行う必要がある場合や、E2Eテストを複数同時に走らせる場合にはこの方法が必須になります。
    2. 複数ブランチの画面を同時に見比べられる 仮にUIの変更を複数同時に進めている場合、ブランチごとにポートを分けておけば、ブラウザで複数のタブを開いて見比べることができます。これも同時に動作確認する必要がある場合には便利です。
    • デメリット
    1. 「今どのポートがどのブランチか」の認知コストが高い 割とストレスになります。開発を進めていくと大量のブランチが立ち上がっていく中で、どのブランチがどのポートで動いているかを覚えるのは大変です。 認知コストの軽減策として、Claude Codeのstatuslineに現在のポートを表示する方法があります。 また、ポートが変わるとChromeのパスワードマネージャーが効かなくなることも小さな影響ながら開発体験に響きます。localhost:3000で保存したログイン情報がlocalhost:3847では候補に出てきません。こちらについてはworktree自体の使い回しで、worktreeごとにポートを決めておけば一度のログインでパスワードなどのログイン情報は保存できるため、使いまわせます。

    選択肢B: 楽観的に同じポートを利用する

    • メリット
    1. ポート管理が不要で、設定を変更する必要がなく、worktree作成時にポートを切り替えるための設定も不要
    2. E2Eテストなどポート固定前提のツールがそのまま使える
    • デメリット
    1. 同時に動作確認できるのは1セッションのみ

    ポート競合が問題になるのは、主にフロントエンドからバックエンドへアクセスする必要がある場合です。フロントエンドがバックエンドのポートを知る必要があるため、両方を同時に起動すると競合します。一方、フロントエンド単体やバックエンド単体の修正であれば、フレームワークが自動で空きポートを割り当ててくれることも多く、あえてポートを固定する必要はありません。

    たとえばViteを利用している場合、デフォルトのポート(5173)が使用中であれば自動的にインクリメントされた空きポートが割り当てられます。フロントエンドの修正が複数同時に走る場合でも、すでに起動している他のworktreeのバックエンドサーバーへアクセスすれば動作確認できるため、この方法で十分なことも多いです。

    このアプローチは「並行でコードを書きつつ、動作確認は1つずつ行う」ワークフローに適しています。ただし、LLMにバックグラウンドで開発サーバーの起動を任せる場合など、どのworktreeのサーバーが起動中か把握しきれなくなることがあります。そうした場合に備えて、ポートを指定してプロセスを停止する手段を用意しておくと便利です。

    lsof -i :3000 -t | xargs kill -9 


    選択肢C: リバースプロキシで振り分ける

    nginxやcaddyなどのリバースプロキシを使い、サブドメインやパスで各worktreeのサーバーに振り分ける方法もあります。

    feature-a.localhost → localhost:3001
    feature-b.localhost → localhost:3002
    • メリット
    1. ブラウザからは意味のあるURLでアクセスでき、認知コストが低い
    • デメリット
    1. プロキシの設定・管理が追加のコストになる

    サブドメインの違いではchromeのパスワードマネージャーも効くため、ポートを切り替える方法よりも開発体験が向上します。 しかし、並行開発のためだけにリバースプロキシを立てるのはやや大げさな気もします。実際に自分はこの方法を試せていません。


    2. DB競合

    DBについて、同じDBを共有するか、worktreeごとに別のDBを立てるかの選択肢があります。スキーマ変更などのマイグレーションを含むタスクも並行で進める場合は、DBを分ける必要に迫られることも多いです。

    選択肢A: 同じDBを共有する

    • メリット
    1. データの同期が不要
    2. シードデータの管理が一元化される
    3. セットアップに時間がかからない
    4. リソース消費が最小限
    • デメリット
    1. 片方のworktreeでのスキーマ変更(マイグレーション)がもう片方に影響する
    2. 場合によっては片方の作業が完全に止まる可能性がある
    3. テストデータの投入・削除が互いに干渉する

    スキーマ変更を伴わないタスク同士(例: フロントエンドの修正が2つ)だったり、スキーマ変更がある場合でも互いに干渉しないようにタスクの順序を工夫できる場合は、DBを共有する方法で十分なことが多いです。


    選択肢B: worktreeごとに異なる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を同時に起動できます。

    • メリット
    1. 完全に独立しており、互いに影響しない スキーマ変更を伴うタスクを安全に並行できる。
    • デメリット
    1. シードデータやマイグレーションの適用を各DBで個別に管理する必要がある
    2. Dockerコンテナを複数起動するため、メモリ消費が増える(ただしイメージは共有されるため、ディスク圧迫の主因はデータボリュームである)

    実際にworktreeごとにDBコンテナを個別に立てる構成を試しましたが、運用上の負担が大きいと感じました。スキーマ適用やseedの投入によりworktree作成時のセットアップが遅くなるうえ、複数リポジトリの開発を掛け持ちしているとDBコンテナが際限なく増えてディスクを圧迫します。不要になったコンテナのクリーンアップも必要で、管理の認知コストが高くなります。必要に迫られれば採用しますが、個人的には積極的に選ぶ方法ではありません。

    選択肢C: 同一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のデータベース名を分けることで、コンテナを増やさずに論理的に分離する方法もあります。

    • メリット
    1. コンテナは1つで済み、リソース消費を抑えられる
    • デメリット
    1. 接続先の切り替えをアプリケーション側で管理する必要がある

     

    3. worktreeの管理

    git worktreeでは.git管理下のファイルは自動的に分離されますが、.gitignore対象のファイルは自分で管理する必要があります。

    対象となるファイル群

    種別 特性
    環境設定 .env, .env.local 機密情報を含む、軽量
    依存パッケージ node_modules/, vendor/, .husky 大容量、再生成可能
    IDE設定 .vscode/settings.json, .idea/ git管理していない個人設定
    submodule libs/shared/ git管理だがworktreeで別途対応が必要

    ファイルの種別ごとに「共有する / 分離する」を判断する必要があります。すべてを一括で同じ方法にするのではなく、特性に応じた選択が必要です。

    手段A: シンボリックリンクで共有する

    具体例: 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にあるファイルへシンボリックリンクを張る方法です。

    • メリット
    1. ディスク容量を節約できる
    2. 環境設定ファイル(.env等)の一元管理ができる
    • デメリット
    1. 片方のworktreeでの変更がもう片方へ即座に反映される
    2. node_modules/を共有すると、依存パッケージの変更で競合する可能性がある


    手段B: worktreeごとに個別管理する(コピー or 再インストール)

    具体例: 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
    • メリット
    1. 完全に独立しており、互いに影響しない
    2. ブランチごとに異なる依存バージョンを扱える
    • デメリット
    1. node_modules/等の容量が大きい場合にディスクを圧迫する
    2. セットアップに時間がかかる

    pnpmを利用している場合、グローバルストアを共有するためディスク消費を大幅に抑えられます。

    submoduleの扱い

    git worktreeではsubmoduleが自動的には初期化されません。worktree作成後に明示的にgit submodule update --initを実行する必要があります。これもセットアップスクリプトに含めておくと漏れがないです。

    その他

    huskyなどの初期化が必要なツールもあるため、worktree作成時のセットアップスクリプトで必要な初期化処理をまとめて行うと便利です。

    devcontainerで環境をまるごと分ける

    ここまで述べてきた課題を根本的に回避するアプローチとして、devcontainerで環境を丸ごと隔離する方法があります。

    概要

    devcontainerではdocker-composeと組み合わせて、アプリケーションコンテナ(フロント+バック)とDBコンテナを定義し、worktreeごとに独立した環境を構築できます。ポート・DB・ファイルの競合問題がそもそも発生しません。

    正直なところ、自分自身はまだdevcontainerを並行開発で本格的に使えていません。セットアップコストの高さに敬遠してしまっている部分もあるので、今後試してみたいと思っています。

    筆者の構成例

    ここまでを踏まえ、現在運用している構成例を紹介します。自分の場合はworktreeをせいぜい5つ程度で運用しており、それに耐えられる構成を考えた結果、以下に落ち着きました。分離の選択肢を多く紹介しましたが、分離にはそれぞれセットアップや管理のコストが伴うため、基本は共有にしておき、必要に迫られたときだけ分離する方が運用コストを低く抑えられるという判断です。

    • ポート: 基本共有
    • DB: 基本共有
    • env: 基本シンボリックリンクで共有
    • node_modules: シンボリックリンクで共有

    ほとんどのタスクはこの構成で問題なく対応できています。

    運用面では、セットアップスクリプトを用意し、上記の設定でworktreeの作成・初期化をまとめて行えるようにしています。このスクリプトはzshのエイリアスに登録してあり、どこからでも実行可能です。worktree自体はタスクごとに削除せず使い回しています。

    修正内容に応じた柔軟な対応も行っています。たとえば、シンボリックリンクで共有している.envを一時的にコピーして書き換えたり、DBも必要に応じて新規データベースを作成して接続先を切り替えたりしています。

    node_modulesの共有についても、ほとんどの場合問題になりません。依存関係は追加されることがあっても削除されることは稀だからです。依存関係の整理などで削除が必要になった場合は、そのworktreeでのみシンボリックリンクを外して個別にインストールすれば対応できます。

    これらの運用は少し面倒に聞こえるかもしれませんが、その操作自体もLLMに任せることで、実際の運用上の負担はかなり軽減されます。セットアップスクリプトもLLMに管理させているため、必要なときに必要な操作をLLMに指示するだけで環境の切り替えや初期化が完了します。

    ※前提として、ポートやDB接続先などがすべて環境変数で管理されている必要があります。

    普段はClaude Code for VS Codeの単一ウィンドウから、複数のworktreeを立てて並行で進めています。

    202605_git_worktree_ts_monorepo_guide_01

    • メインワークツリーのセッションから作業ワークツリーの指定と実装内容を伝えることで、Claude側でcdして切り替えて実装してくれる。ここはSkill化している。
    • コードの差分確認はVSCodeのソース管理セクションで見比べられる。
    • ターミナルは必要に応じて手動でcdして移動するか、VSCodeのアクティビティバーの「ワークツリー」を右クリックして「統合ターミナルで開く」からそのworktreeのターミナルを開く。
    • 開発環境の立ち上げは基本的にClaude Codeのバックグラウンドタスクにまかせ、必要に応じてターミナルでnpm(pnpm) run devする。

    この運用だと、ウィンドウの切り替えが最小限で済み、差分確認やターミナル操作も同一ウィンドウ内で完結するため、複数のworktreeを行き来しながらの開発がスムーズに行えます。

    まとめ

    今回は、git worktreeを活用した並行開発における課題と解決策を整理しました。

    自分自身、考慮すべき点の多さからなかなか踏み出せずにいましたが、実際にやってみると一番シンプルな「基本は共有」という構成で十分に運用できています。本記事で扱った内容は概念的なものが中心であり、実際にはプロジェクトの構成やチームの状況に応じた意思決定が必要です。まずはgit管理外の設定ファイルやセットアップスクリプトを用意して個人で試してみて、そこから徐々にプロジェクト全体の運用ルールへ広げていくのが現実的な進め方でしょう。実際にやってみると、記事だけでは見えてこない課題や工夫が見つかるはずです。

    アジアクエスト株式会社では一緒に働いていただける方を募集しています。
    興味のある方は以下のURLを御覧ください。