cd /news/developer-tools/brunch-gem-isolated-development-envi… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-146939] src=dev.to β†— pub= topic=developer-tools verified=true sentiment=↑ positive

Brunch Gem: Isolated Development Environments for Git Branches and Worktrees

A developer released Brunch, an open-source tool that gives each Git branch and worktree its own isolated development environment, using Docker Compose to assign separate containers, networks, and named volumes per branch. Built on the git-hooks-ext project, which translates Git's low-level reference-transaction hook into semantic lifecycle events, Brunch automatically stops the environment for the branch being left and starts the one for the branch being entered, restoring persistent volumes on return. The idea was first conceived in 2018 but abandoned until the reference-transaction hook made it practical.

by read9 min views1 publishedOct 7, 2026

Switching Git branches changes your code instantly.

It usually doesn't change your development environment.

You may switch from main to a feature branch with a different database schema, different seed data, or different services running in the background. Then you switch back β€” and the source code follows Git, while the rest of the environment is still whatever the previous branch left behind.

With worktrees, it gets even more interesting.

You may have several branches checked out at the same time:

~/my-app
~/my-app-login
~/my-app-payments

Now each of them may need its own database, containers, persistent state, and a different host port.

This becomes especially relevant when IDEs or coding agents use worktrees to work on several tasks in parallel.

I wanted the development environment to follow Git:

branch
    ↓
isolated environment

Switch the branch, switch the environment.

Create another worktree, give it another port and let both environments run in parallel.

Come back to a branch later, and get its previous state back.

That is what Brunch does.

And the idea behind it started in 2018.

Back then, the idea was much smaller.

I wanted every feature branch in a Rails application to have its own database.

If I created:

feature/login

I wanted a development database belonging to feature/login.

If I switched back to:

main

I wanted the database belonging to main again.

The difficult part turned out not to be creating databases.

It was knowing when Git had created, deleted, or switched a branch.

At the time, I couldn't find a reliable way to do that without wrapping Git commands.

And a tool that only works when everyone remembers to use a custom Git wrapper wasn't the solution I wanted.

So I reserved the brunch name on RubyGems and abandoned the idea.

For eight years.

When I returned to the problem, Git had a lower-level hook called reference-transaction.

That eventually became another project of mine, git-hooks-ext.

It translates low-level Git changes into semantic lifecycle events that other tools can react to.

I wrote about the implementation in a separate article:

Git Hooks Ext: The Missing Git Callbacks for Reference Transactions

That article covers the Git internals, edge cases, worktrees, and the limits of what Git currently exposes.

For Brunch, the important part is simpler:

Git repository changes can now become lifecycle events.

Brunch attaches development environments to those events.

The project I had abandoned in 2018 finally became practical.

Brunch runs isolated development environments for Git branches and worktrees.

The simplest model is:

main
    ↓
environment A

feature/login
    ↓
environment B

feature/payments
    ↓
environment C

With Docker Compose, each branch gets its own Compose project, containers, network, and named volumes.

Suppose I am on main.

Its environment is running.

Then I switch:

git switch feature/login

Brunch stops the environment belonging to main and starts the one belonging to feature/login.

Later:

git switch main

and the previous environment comes back, including its persistent volumes.

The source code follows Git.

Now the development environment can follow it too.

Imagine a Rails application where a feature branch contains new migrations.

You switch to it, run the migrations, change some data, and work on the feature.

Then you switch back to main.

Your source code is back on main.

Your database isn't.

Sometimes that is fine.

Sometimes the branch has destructive migrations, incompatible schema changes, different seed data, different service versions, or state in Redis, queues, search indexes, or other infrastructure.

Git isolates the files.

It doesn't isolate the runtime state surrounding them.

Brunch makes that state part of the branch environment.

With a single Git worktree, only one branch can be checked out there at a time.

That means branches can reuse the same host port.

For example:

worktree
β”‚
β”‚ localhost:3000
β”‚
β”œβ”€β”€ main               [running]
β”œβ”€β”€ feature/login      [stopped]
└── feature/payments   [stopped]

Switch branches and the active environment changes.

The port does not have to.

For the problem I originally had in 2018, that would already have been enough.

But worktrees make things more interesting.

Git worktrees let several branches be checked out simultaneously:

~/my-app
~/my-app-login
~/my-app-payments

Now all three applications may need to run at the same time.

They cannot all bind to:

localhost:3000

So Brunch separates two concepts:

The environment belongs to the branch.

The host port belongs to the worktree.

worktree 1
localhost:3000
β”œβ”€β”€ main
└── feature/search

worktree 2
localhost:3001
└── feature/login

worktree 3
localhost:3002
└── feature/payments

In the first worktree, I can switch between main and feature/search.

Both can use port 3000, because only one can be active there at a time.

Meanwhile, the other two worktrees can run simultaneously on ports 3001 and 3002.

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.

Brunch takes care of making the worktrees reachable on different host ports.

Worktrees have been around for a long time, but they are becoming more interesting as IDEs and coding agents use them for parallel work.

Imagine:

agent A β†’ feature/auth
agent B β†’ feature/search
agent C β†’ fix/payments

Git already gives each task its own working tree.

But if all three tasks share the same database, containers, volumes, Redis instance, or port, the isolation is incomplete.

One agent can migrate a database while another is using it.

One can restart a service needed by another.

Two application servers can compete for the same port.

For parallel development, the useful unit of isolation becomes:

worktree
+
branch
+
environment
+
persistent state
+
host port

That is the model Brunch is built around.

Brunch relies on git-hooks-ext for Git lifecycle events, so install that first and make sure ghe is available on your PATH.

Then install Brunch:

gem install brunch

Brunch runs on the host, so it doesn't need to be added to your application's Gemfile.

For a Rails project using Docker Compose, you typically add:

Dockerfile.dev
compose.yaml
brunch.yml

A minimal brunch.yml can be as small as:

compose_file: compose.yaml

A simple Compose configuration may look like this:

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.

If the application uses PostgreSQL instead of SQLite, the same principle applies: the database service and its volume live inside the branch-specific Compose project.

Once the configuration is committed, install the hooks:

brunch install

Check that everything is configured correctly:

brunch doctor

Then activate the environment for the branch you are currently on:

brunch activate

Activation is needed because installing the hooks does not itself cause a Git lifecycle event.

After that, normal branch changes can drive the environment lifecycle.

You can inspect the current state with:

brunch status

list environments grouped by worktree:

brunch list

and print the port assigned to the current worktree:

brunch port

Docker Compose is the default environment manager, and Brunch also supports Podman Compose.

But the core idea is not:

branch β†’ Docker container

It is:

branch β†’ development environment

For example, Brunch can manage a local process:

manager: local_process
command: bin/dev

or project-specific commands:

manager: command

commands:
  create: bin/environment create
  start: bin/environment start
  stop: bin/environment stop
  remove: bin/environment remove

Containers are convenient because they already provide strong isolation, but they are not required by the model.

You can.

Brunch does not make anything possible that Docker Compose fundamentally could not do before.

You can manually create separate Compose projects, volumes, and ports.

You can also manually stop one environment and start another every time you change branches:

git switch feature/login
docker compose -p feature-login up -d

The interesting part is making the lifecycle follow Git automatically.

I don't want changing context to mean:

switch Git branch
remember the previous environment
stop it
choose the next project
choose its port
start it

I want:

git switch feature/login

Git already knows that the context changed.

Brunch reacts to that change.

Branches can be observed through Git's reference machinery.

Worktrees cannot.

Git currently has no native lifecycle hooks for operations such as creating, removing, moving, locking, or unlocking a worktree.

For now, git-hooks-ext provides:

ghe worktree ...

as a frontend for:

git worktree ...

The details of how that works are covered in my git-hooks-ext article.

For Brunch, the practical consequence is simple:

If you want automatic worktree lifecycle handling today, use:

ghe worktree add -b feature/login ../feature-login

instead of plain git worktree add.

If another tool creates the worktree directly, Brunch can still be activated manually inside it:

cd ../feature-login
brunch activate

The wrapper works, but I would rather not need it.

The clean solution is for Git itself to expose worktree lifecycle hooks.

Then IDEs, coding agents, Brunch, and any other tool could use ordinary:

git worktree add ...

and Git could notify interested tools that the worktree was created.

No wrapper would be necessary.

I have started an RFC discussion about adding worktree lifecycle hooks to Git.

If this would be useful in your workflow, please consider joining the discussion:

The most useful contribution is not just a +1.

If your workflow would benefit from native worktree lifecycle hooks, describe the use case. If you have thoughts about the proposed interface, edge cases, or how such hooks should behave, add that feedback as well.

For this kind of change, it is useful to show why the functionality belongs in Git itself instead of another wrapper around git worktree.

For Brunch, native hooks would mean that IDEs and coding agents would not have to know that Brunch exists.

They could use Git normally.

Git would report what happened.

Brunch would react.

That is the main limitation I would like to remove from the current workflow.

I reserved the RubyGems name for Brunch in 2018 because I wanted separate development databases for feature branches.

Eight years later, the idea became broader.

A branch may imply not only different source code, but also a different database schema, data, services, volumes, and runtime configuration.

And with worktrees and parallel coding agents, several of those environments may need to exist at the same time.

The difficult part was never creating another container.

It was connecting the lifecycle of the environment to the lifecycle of Git.

git-hooks-ext provided that missing event layer.

Brunch finally gives those events something useful to manage.

Eight years after reserving the name, I can switch a Git branch and have the development environment switch with it.

── more in #developer-tools 4 stories Β· sorted by recency
── more on @brunch 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/brunch-gem-isolated-…] indexed:0 read:9min 2026-10-07 Β· β€”