Introduction
Switching branches and worktrees in Git changes the code we're working on. This allows us to work on multiple things at the same time. But what it doesn't switch is the environment.
Feature branches, bug-fix branches, and branches used for code review or refactoring often have different migrations. Different seeds. Different data that we create to test our solutions. They may also have different background jobs or different gems in their Gemfiles.
In general, branches and worktrees should have separate environments.
When working with Ruby on Rails, we already have three environments by default: development, test, and production. But what we really need is a separate environment for each branch.
That's exactly what the Brunch gem provides.
It not only lets us manage these environments, but also ties them to Git using hooks.
The idea dates back to 2018
Back then, the scope was much narrower. I wanted to create a gem that would isolate databases, creating a separate database for each branch, so I wouldn't have to keep dropping and recreating the database every time I switched branches.
I reserved the name Brunch on RubyGems.
The problem, however, was that Git lacked the necessary hooks. Of course, I could have built a wrapper around Git, but I decided that such a solution was unlikely to catch on.
So I abandoned the idea. For eight years.
This year, I discovered that Git has had a reference-transaction hook since 2020, and that it could be used for this purpose. To make it work, I also had to create git-hooks-ext, which I described in a separate article.
The idea was back.
What does Brunch do?
Brunch runs isolated environments for Git branches and worktrees. It connects them to Git hooks so that environment lifecycles can follow branch changes automatically.
Each worktree is assigned a port on which the application runs. Within a single worktree, that port is used by whichever branch is currently active.
Let's take a situation where we have just one worktree:
worktree
│
│ localhost:3000
│
├── main [running]
├── feature/login [stopped]
└── feature/payments [stopped]
Containers are automatically created and removed when branches are created and deleted. When switching branches, the container belonging to the previously active branch is stopped, and the container for the branch we're switching to is started.
With multiple worktrees, things get a little more complex because several branches can be active at the same time.
Among other things, this allows multiple AI coding agents to work on different branches in parallel.
That's why each worktree gets its own port:
worktree 1
localhost:3000
├── main
└── feature/search
worktree 2
localhost:3001
└── feature/login
worktree 3
localhost:3002
└── feature/payments
Brunch exposes the assigned port through BRUNCH_PORT, so Docker Compose can use it like this:
services:
web:
ports:
- "127.0.0.1:${BRUNCH_PORT}:3000"
Rails can keep listening on port 3000 inside the container, and Brunch takes care of making the worktrees reachable on different host ports.
Installing Brunch
Brunch relies on git-hooks-ext, so install that first and make sure ghe is available on your PATH.
Then install Brunch:
gem install brunch
Brunch doesn't need to be added to your application's Gemfile, although you can also add it to the development group.
For a Rails project using Docker Compose, you typically add:
Dockerfile.devcompose.yamlbrunch.yml
A minimal brunch.yml:
compose_file: compose.yaml
Here's an example configuration for Ruby on Rails:
services:
web:
build:
context: .
dockerfile: Dockerfile.dev
command: >
sh -c 'bin/rails db:prepare &&
exec bin/rails server -b 0.0.0.0 -p 3000'
volumes:
- .:/rails
- storage:/rails/storage
ports:
- "127.0.0.1:${BRUNCH_PORT}:3000"
volumes:
storage:
The bind mount exposes the current worktree to Rails. The named volume belongs to the branch-specific Compose project. And BRUNCH_PORT maps the application to the port assigned to the worktree.
Once you've committed the configuration, install the hooks, check that everything is configured correctly, and then activate the environment for the branch you're currently on:
brunch install
brunch doctor
brunch activate
Activation is needed because installing the hooks does not itself trigger a Git lifecycle event.
Now you can inspect the current state, list Brunch environments, or print the port assigned to the current worktree:
brunch status
brunch list
brunch port
Worktrees must use a wrapper
Branches can be observed through Git's reference machinery, but Git still has no native lifecycle hooks for worktree operations.
For now, git-hooks-ext provides a wrapper: ghe worktree ....
Instead of plain git worktree add, use:
ghe worktree add -b feature/login ../feature-login
If another tool creates the worktree directly, Brunch can still be activated manually:
cd ../feature-login
brunch activate
The wrapper works, but I'd rather rely entirely on Git hooks without having to use it.
The missing piece belongs in Git
The clean solution is for Git itself to expose worktree lifecycle hooks. Then IDEs, coding agents, Brunch, and other tools could simply use git worktree add. No wrapper would be necessary.
There is an ongoing RFC discussion about adding worktree lifecycle hooks to Git. If this would be useful in your workflow, please join the discussion: