🪮

Git Worktreeでブランチごとにデータベースも分離 —— Docker Volume スナップショットの活用

に公開

LayerX Tech Advent Calendar 2025の15日目の記事です。

バクラク事業部 ソフトウェアエンジニアの @upamune です。

今日は、ローカル開発においての困りごとである、Git Worktreeとデータベースなどの永続化ミドルウェアの組み合わせ問題をいい感じにした話をします。


はじめに

AI Coding Agentの登場で、複数の機能を並行して開発する機会が増えました。Agentに任せている間に別の作業を進めたり、レビュー待ちのブランチを放置して次のタスクに取りかかったり。こうした並行開発では、git worktreeが便利です。ブランチごとに独立したディレクトリを持てるので、git stashgit checkoutを繰り返す必要がなくなります。

ただ、git worktreeだけでは解決しない問題があります。DBの状態です。

例えば、Docker ComposeでMySQLを起動してローカル開発をしている環境では、複数のworktreeが同じDockerボリュームを共有してしまいます。 feature-a で追加したカラムが feature-b には存在しない、マイグレーションを戻したり当て直したりという様に、コードは分離できてもDBの状態が足を引っ張ります。

この記事では、git worktreeとDocker Volumeのスナップショットを組み合わせて、ブランチごとにDBの状態も分離する方法を紹介します。

問題: ブランチ切替時のDB状態不整合

複数の機能ブランチを並行開発している状況を考えてみてください。

たとえばこんな状況です。

  • feature-aブランチでusersテーブルにカラムを追加し、マイグレーションを実行した
  • 急ぎの修正でfeature-bブランチに切り替えた
  • feature-bにはそのカラムがないので、コードとDBの状態が不整合状態になる
    • DBからコードやドキュメントの自動生成を行っている場合は特に問題に

こうなると「マイグレーションを戻す → ブランチ切替 → 再度マイグレーション」という手順が必要になります。さらに厄介なのは、開発用のテストデータまで影響を受けること。feature-aで作り込んだデータがfeature-bで壊れたり、その逆も起きます。

ブランチ切替のたびにDBの状態を気にするのは、地味にストレスです。

解決策の概要

ここで私たちが採用した解決策はDocker Volumeを利用したシンプルなものでした。

  1. git worktreeでブランチごとに独立した作業ディレクトリを作る
  2. Docker VolumeのスナップショットでDBの状態を複製する
  3. 各worktreeに専用のvolumeを割り当てる

従来の構成では、ブランチを切り替えても同じvolumeを参照してしまいます。worktreeとvolumeを組み合わせることで、ディレクトリごとに独立したDB環境を持てます。

実現するための具体的な手順を解説していきます。

手順

1. git worktreeでブランチ用のディレクトリを作成

$ git worktree add ../myapp-feature-a feature-a

作成先は../myapp-feature-a.git/worktrees/myapp-feature-aなど、どこでも構いません。

2. Docker Volumeスナップショットの取得

既存のvolumeを新しいvolumeにコピーします。

コピー前にコンテナを止めておくと安全です。

$ docker compose stop mysql

MySQLのvolumeをコピーします。

$ docker volume create myapp-mysql8-feature-a # volume 作成
# volume のデータコピー
$ docker run --rm \
  -v myapp-mysql8:/from \
  -v myapp-mysql8-feature-a:/to \
  alpine sh -c "cp -a /from/* /to/" 

Docker Volumeは通常の操作では直接アクセスできませんが、コンテナにマウントすれば普通のディレクトリとして扱えます。そのため、 alpine のコンテナを経由して新しく作成した別ボリュームにデータをコピーしています。

cp -aで属性を保持したままコピーするので、データファイルも正しくコピーされます。

3. worktreeとvolumeの紐付け

作成したworktree側では、新しく作成したvolumeを参照させたいです。

それを実現するために、worktreeごとにdocker-compose.override.ymlを配置して、volume名を差し替えます。

Docker Composeには、docker-compose.ymlと同じディレクトリにdocker-compose.override.ymlがあると自動でマージしてくれる機能があります。Compose V2(docker composeコマンドを使う形式)ではcompose.yamlcompose.override.yamlという命名も認識されます。詳しくは公式ドキュメントを参照してください。

たとえば、ベースとなるdocker-compose.ymlが以下のような構成だとします。

# docker-compose.yml
services:
  mysql:
    container_name: mysql
    image: mysql:8
    volumes:
      - myapp-mysql8:/var/lib/mysql
      - ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf

volumes:
  myapp-mysql8:
    driver: local

この場合、worktree用のdocker-compose.override.ymlは以下のように書けます。

# myapp-feature-a/docker-compose.override.yml
services:
  mysql:
    volumes:
      # myapp-mysql8 を myapp-mysql8-feature-a に書き換えている
      - myapp-mysql8-feature-a:/var/lib/mysql
      - ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf

volumes:
  myapp-mysql8-feature-a:
    driver: local

ポイントは、volume名だけを差し替えていること。他の設定(image、container_nameなど)はベースのdocker-compose.ymlからそのまま引き継がれます。

docker-compose.override.yml.gitignoreに追加しておきましょう。worktreeごとに異なる設定を持つので、リポジトリにコミットする必要はありません。

4. 運用フロー

新しいブランチを切るときは以下のようにします。

# 1. worktree作成
git worktree add ../myapp-feature-x feature-x

# 2. volume作成 & コピー
docker compose stop mysql
docker volume create myapp-mysql8-feature-x
docker run --rm \
  -v myapp-mysql8:/from \
  -v myapp-mysql8-feature-x:/to \
  alpine sh -c "cp -a /from/* /to/"
docker compose start mysql

# 3. override.ymlを自動生成してworktreeに配置
# 後述するため、ここでは省略

# 4. 新しいworktreeで開発開始
cd ../myapp-feature-x
docker 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" << EOF
services:
  mysql:
    volumes:
      - myapp-mysql8-${safe_branch_name}:/var/lib/mysql
      - ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf

volumes:
  myapp-mysql8-${safe_branch_name}:
    driver: local
EOF

worktree削除時には、関連する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 volume rm myapp-mysql8-feature-x

結果

この構成にしてから、ブランチ間の移動が cd とコンテナ起動だけになりました。

これによってDBの状態不整合に悩まされることがなくなり、開発体験が向上しました。

  • マイグレーションの状態を気にせず、すぐに別ブランチで作業できる
  • 「このブランチではこういうデータで試したい」という実験がやりやすくなった
  • DBマイグレーションを含むプルリクエストを手元で動かすときにも便利。普段使っている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のアドベントカレンダーは続いていくので、是非お楽しみに〜!

LayerX

Discussion