8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Claude Codeのカスタムコマンドで並列開発できるローカル環境を作ってみた

8
Last updated at Posted at 2025-12-10

この記事は株式会社カオナビ Advent Calendar 2025の11日目の記事です。

はじめに

Claude Codeで開発していて 実装のスピードは前より格段に上がったことで、「レビュー待ちの間に次のチケットを進めたい」「複数の機能を並行して実装したい」といったニーズが増えてきました。
しかし、いざ並列開発しようとすると、開発環境が足を引っ張ることに気づきました。
「ユニットテストの実行できない」「ローカルで動作確認できない」ため、結局は1つチケットずつ作業して、レビュー指摘を受けたら変更をスタッシュして、元のブランチに戻して対応、終わったらまたさっきのブランチに戻りスタッシュを取り込んで…
ということを繰り返していましたが、開発スピードが上がったことでその頻度が増えることにストレスを感じていました。

そこで、並列開発できるカスタムコマンドをClaudeで作ってみようと思い立ったのです。

開発環境と課題

私が開発しているのは、Laravelのアプリケーションです。
ほかにも複数のLaravelのアプリケーションのコンテナを同時に起動しており、DBやRedisを共有しています。

ホストマシンのディレクトリ構成:

~/work/ 
├── local-dev/          # Docker環境を管理するリポジトリ
│   └── compose.yaml
├── app-A/              # アプリAのソースコード(今回の対象)
└── app-B/              # アプリBのソースコード

Docker環境の構成:

Docker Network (local-dev)
├── app-A コンテナ    → ~/work/app-A/ をマウント
├── app-B コンテナ    → ~/work/app-B/ をマウント
├── MySQL
└── Redis

このような構成になっているため、app-Aのディレクトリでgit worktreeを使って別ブランチのワークスペースを作っても、app-Aコンテナは1つしかなく、元のディレクトリ(~/work/app-A/)をマウントしたままです。

つまり、worktree環境(例:~/work/worktree-TICKET-925/)でコードを変更しても、Dockerコンテナは元のディレクトリを参照し続けるため、テストも動作確認もできないという状況になるのです。
困っちゃいますね。

git worktreeだけで解決できない問題まとめ

  • compose.yamlは元のディレクトリ(app-A/)をマウントする設定になっている
  • worktree環境でコードを変更しても、Docker上では元のディレクトリが参照される

私がしたいこと

(正直 git worktreeのコマンドも覚えられないので)パッとチケット番号を伝えたら、直前まで作業していた場所とは別の作業スペースができて、コンテナも用意されて、新しいチケットの開発がすぐにできるようにしたい。
レビュー待ち中に別のチケットの作業を進めていても、レビュー指摘が飛んできたら元の作業スペースに戻って修正してテストしてコミットしてということができるようになります。

解決策:ClaudeCodeカスタムコマンドで全自動化

各worktreeごとに独立したDockerコンテナを起動し、それぞれのディレクトリをマウントしてくれるカスタムコマンドを用意することにしました。
ただし、今回はMySQLやRedisなどのコンテナは個別に作成せず、共有します。

ホストマシンのディレクトリ構成(worktree追加後):

~/work/
├── local-dev/               # Docker環境を管理するリポジトリ
│   └── compose.yaml
├── app-A/                   # メインの作業ディレクトリ
├── worktree-TICKET-925/     # ← git worktreeで追加
├── worktree-TICKET-1025/    # ← git worktreeで追加
└── app-B/

Docker環境の構成(コンテナ追加後):

Docker Network (local-dev_default)
├── app-A コンテナ           → ~/work/app-A/ をマウント
├── app-A' コンテナ          → ~/work/worktree-TICKET-925/ をマウント ← 追加
├── app-A'' コンテナ         → ~/work/worktree-TICKET-1025/ をマウント ← 追加
├── app-B コンテナ
├── MySQL(共有)
└── Redis(共有)

※ 実際のコンテナ名は ticket-925-webticket-1025-web などになります

.claude/commands/worktree-docker.mdを作成し、以下のように処理を定義しました:

---
description: JIRAチケット番号からworktree + Docker環境を作成
args:
  - name: ticket_number
    description: JIRAチケット番号 (例: TICKET-925)
    required: true
---

JIRAチケット {{ticket_number}} のworktree + Docker環境を作成します。

### 処理フロー

1. MCPサーバーを使ってJIRAからチケット情報を取得
2. 既存ブランチを検索、なければ新規作成
3. worktreeを作成
4. Dockerネットワークを検出
5. 空きポートを自動採番(8901, 9001, 9101...)
6. docker-compose.override.ymlを生成
7. Dockerコンテナを起動
8. .envファイルを自動コピー
9. セットアップ手順を表示

これにより、以下のようなコマンド1つで環境が構築できるようになりました:

/worktree-docker TICKET-925

実行すると、以下の処理が自動で行われます:

  • JIRAからチケット情報を取得
  • ブランチ名を自動生成(feature/nakata_TICKET-925_短い説明
  • worktree作成
  • Docker環境構築(ポート自動採番、設定ファイル生成)
  • コンテナ起動
  • .envファイルコピー(.gitignoreされており、worktree環境には.envファイルがないため)

作成したカスタムコマンド

今回、3つのカスタムコマンドを作成しました:

1. /worktree-docker

worktree + Docker環境を一括作成します。

  • JIRAからチケット情報を自動取得
  • ブランチの自動生成または既存ブランチの検出
  • ポート番号の自動採番
  • docker-compose.override.ymlの生成
  • コンテナの起動

2. /worktree-remove

worktree + Docker環境を削除します。

  • Dockerコンテナの停止・削除
  • worktreeディレクトリの削除
  • 未コミットの変更を警告

3. /worktree-list

現在のworktree一覧とDocker状態を表示します。

  • 各worktreeのブランチ情報
  • Dockerコンテナの起動状態
  • アクセスURL
  • 未コミット変更の警告

コード例を下記リポジトリに置いておきます。
https://github.com/nakata-midori/custom-command/tree/main

カスタムコマンドを作るにあたって学んだこと

その1:既存の設定を引き継いでコンテナ作成できるdocker-compose.override.ymlとポート設定の仕様

docker-compose.override.ymlを使うことで、既存のdocker-compose.yamlを引き継いで、一部設定を変更したコンテナを作成することができました。

  1. 独立したコンテナ名:チケット番号ごとにコンテナを分離
  2. volume:git worktreeで作成した作業ディレクトリのソースをマウント
  3. 独立したポート:元の環境(8888番)とは異なる空いているポートを探して指定。!overrideタグがないと元のcompose.yamlの設定とマージされてしまい、8888番ポートも使おうとしてエラーになるということがわかりました。
  4. external network:既存のDockerネットワークに参加することで、共有のMySQL/Redisにアクセス

その2:プロジェクト名の競合

元の環境でdocker compose upすると、なぜかworktree環境のコンテナがRecreatedと表示されて再起動されてしまいました。

原因は、Docker Composeがディレクトリ名をプロジェクト名として使うため、設定を共有してしまっていたことでした。

worktree環境のコンテナ起動時に--project-nameオプションで明示的にプロジェクト名を分離することで、互いに影響しないようにできました。

注意点として、プロジェクト名は小文字のみ使用可能です。

その3:元のコンテナに設定されているコンテナ間の依存設定を無効化する方法

元のcompose.yamlでdepends_onが定義されているため、worktree環境のコンテナを起動すると自動的にMySQL/Redisも起動しようとして、ポート競合でエラーになりました。

override.ymlでdepends_on: []を指定しても無効化できませんでした。

worktree環境のコンテナ起動時に--no-depsオプションを使用することで、依存サービスを無視して、指定したサービスだけ起動できました。

実際に使ってみた結果

vendorとnode_modulesがない

worktree環境では、vendor/node_modules/ディレクトリが存在しません。これらは.gitignoreに含まれているためです。そりゃそうだ。
そのため下記のコマンドの実行が必要でした。

# PHP依存関係
docker exec ticket-925-web composer install

# Node依存関係
npm install

# フロントエンドビルド
npm run dev

これらのコマンドもカスタムコマンドの中に含めても良かったのですが、これらは元のコンテナでもよく実行しているコマンドなので、個別に実行でもいいかなと含めませんでした。

DNSエラー

composer installやnpm installでCould not resolve hostエラーが
出ることがありました。

原因は完全には特定できていませんが、以下の状況で発生しやすい気がしています:

  • Macのスリープ復帰後

  • Docker Desktopの長時間起動

  • ネットワーク環境の変化(Wi-Fi切り替えなど)

    解決方法:
    Docker Desktopを再起動することで解決しました。

GTR(Git Worktree Runner) でいいのでは

自分でカスタムコマンドを作ってみた後に、GTR(Git Worktree Runner) の存在を知りました。
GTRはgit worktreeをさらに使いやすく手間のかかる環境構築を簡単にしてくれるツールで、
フックという機能があり、worktree作成後にコンテナ起動などの任意のコマンドを実行できるらしいです。

しかし、自分が作りたい並列開発環境の構築には不十分だったのではないかと思ってます。:

  1. ポート番号の動的採番が困難 - 既存worktreeの使用ポートを追跡して空きポートを見つける処理をシェルスクリプトで書くのは複雑
  2. docker-compose.override.ymlの動的生成が難しい - worktreeの絶対パス、ポート番号、コンテナ名を埋め込んだYAMLをスクリプトで生成するのは保守性が低い
  3. エラーハンドリングが不十分 - ポート競合、ネットワーク不在、コンテナ起動失敗などを適切に処理するのが困難

まとめ

今回カスタムコマンドで並列開発環境を作るようにしてみましたが、もっとスマートな方法があるのではないかと疑念が晴れません。
gtrとClaudeのカスタムコマンドを併用すればもっとスマートになるかもしれないので進展があったら追記します。
もしもっと良い方法をご存知の方がいたら教えてもらえるととても助かります。

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?