
LayerX Tech Advent Calendar 2025の15日目の記事です。
バクラク事業部 ソフトウェアエンジニアの@upamune です。
今日は、ローカル開発においての困りごとである、Git Worktreeとデータベースなどの永続化ミドルウェアの組み合わせ問題をいい感じにした話をします。
AI Coding Agentの登場で、複数の機能を並行して開発する機会が増えました。Agentに任せている間に別の作業を進めたり、レビュー待ちのブランチを放置して次のタスクに取りかかったり。こうした並行開発では、git worktreeが便利です。ブランチごとに独立したディレクトリを持てるので、git stashやgit checkoutを繰り返す必要がなくなります。
ただ、git worktreeだけでは解決しない問題があります。DBの状態です。
例えば、Docker ComposeでMySQLを起動してローカル開発をしている環境では、複数のworktreeが同じDockerボリュームを共有してしまいます。feature-a で追加したカラムがfeature-b には存在しない、マイグレーションを戻したり当て直したりという様に、コードは分離できてもDBの状態が足を引っ張ります。
この記事では、git worktreeとDocker Volumeのスナップショットを組み合わせて、ブランチごとにDBの状態も分離する方法を紹介します。
複数の機能ブランチを並行開発している状況を考えてみてください。
たとえばこんな状況です。
feature-aブランチでusersテーブルにカラムを追加し、マイグレーションを実行したfeature-bブランチに切り替えたfeature-bにはそのカラムがないので、コードとDBの状態が不整合状態になるこうなると「マイグレーションを戻す → ブランチ切替 → 再度マイグレーション」という手順が必要になります。さらに厄介なのは、開発用のテストデータまで影響を受けること。feature-aで作り込んだデータがfeature-bで壊れたり、その逆も起きます。
ブランチ切替のたびにDBの状態を気にするのは、地味にストレスです。
ここで私たちが採用した解決策はDocker Volumeを利用したシンプルなものでした。
従来の構成では、ブランチを切り替えても同じvolumeを参照してしまいます。worktreeとvolumeを組み合わせることで、ディレクトリごとに独立したDB環境を持てます。
実現するための具体的な手順を解説していきます。
$git worktreeadd../myapp-feature-a feature-a作成先は../myapp-feature-aや.git/worktrees/myapp-feature-aなど、どこでも構いません。
既存のvolumeを新しいvolumeにコピーします。
コピー前にコンテナを止めておくと安全です。
$docker compose stop mysqlMySQLのvolumeをコピーします。
$docker volume create myapp-mysql8-feature-a# volume 作成# volume のデータコピー$docker run--rm\-v myapp-mysql8:/from\-v myapp-mysql8-feature-a:/to\ alpinesh-c"cp -a /from/* /to/"Docker Volumeは通常の操作では直接アクセスできませんが、コンテナにマウントすれば普通のディレクトリとして扱えます。そのため、alpine のコンテナを経由して新しく作成した別ボリュームにデータをコピーしています。
cp -aで属性を保持したままコピーするので、データファイルも正しくコピーされます。
作成したworktree側では、新しく作成したvolumeを参照させたいです。
それを実現するために、worktreeごとにdocker-compose.override.ymlを配置して、volume名を差し替えます。
Docker Composeには、docker-compose.ymlと同じディレクトリにdocker-compose.override.ymlがあると自動でマージしてくれる機能があります。Compose V2(docker composeコマンドを使う形式)ではcompose.yamlとcompose.override.yamlという命名も認識されます。詳しくは公式ドキュメントを参照してください。
たとえば、ベースとなるdocker-compose.ymlが以下のような構成だとします。
# docker-compose.ymlservices:mysql:container_name: mysqlimage: mysql:8volumes:- myapp-mysql8:/var/lib/mysql- ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnfvolumes:myapp-mysql8:driver: localこの場合、worktree用のdocker-compose.override.ymlは以下のように書けます。
# myapp-feature-a/docker-compose.override.ymlservices:mysql:volumes:# myapp-mysql8 を myapp-mysql8-feature-a に書き換えている- myapp-mysql8-feature-a:/var/lib/mysql- ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnfvolumes:myapp-mysql8-feature-a:driver: localポイントは、volume名だけを差し替えていること。他の設定(image、container_nameなど)はベースのdocker-compose.ymlからそのまま引き継がれます。
docker-compose.override.ymlは.gitignoreに追加しておきましょう。worktreeごとに異なる設定を持つので、リポジトリにコミットする必要はありません。
コンテナの同時起動について
今回の設定ではcontainer_name を固定しているため、複数のworktreeで同時にコンテナを起動することはできません(ポートの競合も発生します)。
基本的には「作業するworktreeで起動し、別のworktreeに移るときは停止する」という運用を想定しています。
もし複数の環境を同時に立ち上げたい場合は、docker-compose.override.yml でcontainer_name やports(公開ポート)もブランチごとにユニークになるよう書き換えることで対応可能です。
新しいブランチを切るときは以下のようにします。
# 1. worktree作成git worktreeadd../myapp-feature-x feature-x# 2. volume作成 & コピーdocker compose stop mysqldocker volume create myapp-mysql8-feature-xdocker run--rm\-v myapp-mysql8:/from\-v myapp-mysql8-feature-x:/to\ alpinesh-c"cp -a /from/* /to/"docker compose start mysql# 3. override.ymlを自動生成してworktreeに配置# 後述するため、ここでは省略# 4. 新しいworktreeで開発開始cd../myapp-feature-xdocker compose up-d毎回手動で実行するのは面倒なので、シェルスクリプトにまとめておくと便利です。私たちのチームでは、worktree作成・volumeコピー・docker-compose.override.ymlの自動生成を一括で行うスクリプトを用意しています。
ポイントはdocker-compose.override.ymlの自動生成です。ブランチ名からvolume名を決定し、以下のようなファイルを生成します。
# ブランチ名をvolume名に使える形式に変換(スラッシュをハイフンに)safe_branch_name="${branch_name//\//-}"# docker-compose.override.ymlを生成cat>"$worktree_dir/docker-compose.override.yml"<<EOFservices: mysql: volumes: - myapp-mysql8-${safe_branch_name}:/var/lib/mysql - ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnfvolumes: myapp-mysql8-${safe_branch_name}: driver: localEOFworktree削除時には、関連するDockerボリュームとdocker-compose.override.ymlも一緒に削除するようにしておくと、ゴミが残りません。
ブランチを切り替えるときは、worktreeを使っているのでブランチ切替は不要です。ディレクトリを移動するだけで済みます。
cd../myapp-feature-a# このディレクトリのdocker-compose.override.ymlが使われるdocker compose up-d不要になったworktreeとvolumeは以下のコマンドで削除できます。
# worktree削除git worktree remove../myapp-feature-x# volume削除docker volumerm myapp-mysql8-feature-xこの構成にしてから、ブランチ間の移動がcd とコンテナ起動だけになりました。
これによってDBの状態不整合に悩まされることがなくなり、開発体験が向上しました。
ディスク容量は増えますが、開発用途であればMySQLのデータが数GB程度なら問題にならないでしょう。気になる場合は不要になったworktreeとvolumeをこまめに削除すればOKです。
なお、この手法ではブランチ間でDBのデータをマージする機能は提供していません。worktree環境で作り込んだデータをmainブランチの環境に持っていきたい場合は、worktree環境のvolumeをmainブランチの環境にコピーし直すことで対応できます。ですが、自分はそういった場面に遭遇したことがないので、実際に試したことはありません。
従来は「マイグレーションを戻す → ブランチ切替 → 再度マイグレーション」という手順が必要でしたが、この構成ではcd で別のworktreeに移動してdocker compose up -d するだけで済みます。
今回はMySQLを例に挙げましたが、PostgreSQLやRedis、Elasticsearchなど、Docker Volumeでデータを永続化しているミドルウェアであれば同じ手法が使えます。私たちはRedisやLocalStackも同様にブランチごとに分離して運用しています。
AI Coding Agent を利用するようになり、複数の機能を並行開発する機会が多い方は、ぜひ試してみてください。
今後もLayerXのアドベントカレンダーは続いていくので、是非お楽しみに〜!
バッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。
