{"slug": "brunch-gem-isolated-development-environments-for-git-branches-and-worktrees", "title": "Brunch Gem: Isolated Development Environments for Git Branches and Worktrees", "summary": "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.", "body_md": "Switching Git branches changes your code instantly.\n\nIt usually doesn't change your development environment.\n\nYou 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.\n\nWith worktrees, it gets even more interesting.\n\nYou may have several branches checked out at the same time:\n\n```\n~/my-app\n~/my-app-login\n~/my-app-payments\n```\n\nNow each of them may need its own database, containers, persistent state, and a different host port.\n\nThis becomes especially relevant when IDEs or coding agents use worktrees to work on several tasks in parallel.\n\nI wanted the development environment to follow Git:\n\n```\nbranch\n    ↓\nisolated environment\n```\n\nSwitch the branch, switch the environment.\n\nCreate another worktree, give it another port and let both environments run in parallel.\n\nCome back to a branch later, and get its previous state back.\n\nThat is what **[Brunch](https://github.com/ciembor/brunch)** does.\n\nAnd the idea behind it started in 2018.\n\nBack then, the idea was much smaller.\n\nI wanted every feature branch in a Rails application to have its own database.\n\nIf I created:\n\n```\nfeature/login\n```\n\nI wanted a development database belonging to `feature/login`.\n\nIf I switched back to:\n\n```\nmain\n```\n\nI wanted the database belonging to `main` again.\n\nThe difficult part turned out not to be creating databases.\n\nIt was knowing when Git had created, deleted, or switched a branch.\n\nAt the time, I couldn't find a reliable way to do that without wrapping Git commands.\n\nAnd a tool that only works when everyone remembers to use a custom Git wrapper wasn't the solution I wanted.\n\nSo I reserved the `brunch` name on RubyGems and abandoned the idea.\n\nFor eight years.\n\nWhen I returned to the problem, Git had a lower-level hook called `reference-transaction`.\n\nThat eventually became another project of mine, **[git-hooks-ext](https://github.com/ciembor/git-hooks-ext)**.\n\nIt translates low-level Git changes into semantic lifecycle events that other tools can react to.\n\nI wrote about the implementation in a separate article:\n\n[Git Hooks Ext: The Missing Git Callbacks for Reference Transactions](https://dev.to/ciembor/git-hooks-ext-the-missing-git-callbacks-for-reference-transactions-2l3l)\n\nThat article covers the Git internals, edge cases, worktrees, and the limits of what Git currently exposes.\n\nFor Brunch, the important part is simpler:\n\nGit repository changes can now become lifecycle events.\n\nBrunch attaches development environments to those events.\n\nThe project I had abandoned in 2018 finally became practical.\n\nBrunch runs isolated development environments for Git branches and worktrees.\n\nThe simplest model is:\n\n```\nmain\n    ↓\nenvironment A\n\nfeature/login\n    ↓\nenvironment B\n\nfeature/payments\n    ↓\nenvironment C\n```\n\nWith Docker Compose, each branch gets its own Compose project, containers, network, and named volumes.\n\nSuppose I am on `main`.\n\nIts environment is running.\n\nThen I switch:\n\n```\ngit switch feature/login\n```\n\nBrunch stops the environment belonging to `main` and starts the one belonging to `feature/login`.\n\nLater:\n\n```\ngit switch main\n```\n\nand the previous environment comes back, including its persistent volumes.\n\nThe source code follows Git.\n\nNow the development environment can follow it too.\n\nImagine a Rails application where a feature branch contains new migrations.\n\nYou switch to it, run the migrations, change some data, and work on the feature.\n\nThen you switch back to `main`.\n\nYour source code is back on `main`.\n\nYour database isn't.\n\nSometimes that is fine.\n\nSometimes the branch has destructive migrations, incompatible schema changes, different seed data, different service versions, or state in Redis, queues, search indexes, or other infrastructure.\n\nGit isolates the files.\n\nIt doesn't isolate the runtime state surrounding them.\n\nBrunch makes that state part of the branch environment.\n\nWith a single Git worktree, only one branch can be checked out there at a time.\n\nThat means branches can reuse the same host port.\n\nFor example:\n\n```\nworktree\n│\n│ localhost:3000\n│\n├── main               [running]\n├── feature/login      [stopped]\n└── feature/payments   [stopped]\n```\n\nSwitch branches and the active environment changes.\n\nThe port does not have to.\n\nFor the problem I originally had in 2018, that would already have been enough.\n\nBut worktrees make things more interesting.\n\nGit worktrees let several branches be checked out simultaneously:\n\n```\n~/my-app\n~/my-app-login\n~/my-app-payments\n```\n\nNow all three applications may need to run at the same time.\n\nThey cannot all bind to:\n\n```\nlocalhost:3000\n```\n\nSo Brunch separates two concepts:\n\n**The environment belongs to the branch.**\n\n**The host port belongs to the worktree.**\n\n```\nworktree 1\nlocalhost:3000\n├── main\n└── feature/search\n\nworktree 2\nlocalhost:3001\n└── feature/login\n\nworktree 3\nlocalhost:3002\n└── feature/payments\n```\n\nIn the first worktree, I can switch between `main` and `feature/search`.\n\nBoth can use port `3000`, because only one can be active there at a time.\n\nMeanwhile, the other two worktrees can run simultaneously on ports `3001` and `3002`.\n\nBrunch exposes the assigned port through:\n\n```\nBRUNCH_PORT\n```\n\nso Docker Compose can use it like this:\n\n```\nservices:\n  web:\n    ports:\n      - \"127.0.0.1:${BRUNCH_PORT}:3000\"\n```\n\nRails can keep listening on port `3000` inside the container.\n\nBrunch takes care of making the worktrees reachable on different host ports.\n\nWorktrees have been around for a long time, but they are becoming more interesting as IDEs and coding agents use them for parallel work.\n\nImagine:\n\n```\nagent A → feature/auth\nagent B → feature/search\nagent C → fix/payments\n```\n\nGit already gives each task its own working tree.\n\nBut if all three tasks share the same database, containers, volumes, Redis instance, or port, the isolation is incomplete.\n\nOne agent can migrate a database while another is using it.\n\nOne can restart a service needed by another.\n\nTwo application servers can compete for the same port.\n\nFor parallel development, the useful unit of isolation becomes:\n\n```\nworktree\n+\nbranch\n+\nenvironment\n+\npersistent state\n+\nhost port\n```\n\nThat is the model Brunch is built around.\n\nBrunch relies on `git-hooks-ext` for Git lifecycle events, so install that first and make sure `ghe` is available on your `PATH`.\n\nThen install Brunch:\n\n```\ngem install brunch\n```\n\nBrunch runs on the host, so it doesn't need to be added to your application's `Gemfile`.\n\nFor a Rails project using Docker Compose, you typically add:\n\n```\nDockerfile.dev\ncompose.yaml\nbrunch.yml\n```\n\nA minimal `brunch.yml` can be as small as:\n\n```\ncompose_file: compose.yaml\n```\n\nA simple Compose configuration may look like this:\n\n```\nservices:\n  web:\n    build:\n      context: .\n      dockerfile: Dockerfile.dev\n\n    command: >\n      sh -c 'bin/rails db:prepare &&\n      exec bin/rails server -b 0.0.0.0 -p 3000'\n\n    volumes:\n      - .:/rails\n      - storage:/rails/storage\n\n    ports:\n      - \"127.0.0.1:${BRUNCH_PORT}:3000\"\n\nvolumes:\n  storage:\n```\n\nThe bind mount exposes the current worktree to Rails.\n\nThe named volume belongs to the branch-specific Compose project.\n\nAnd `BRUNCH_PORT` maps the application to the port assigned to the worktree.\n\nIf the application uses PostgreSQL instead of SQLite, the same principle applies: the database service and its volume live inside the branch-specific Compose project.\n\nOnce the configuration is committed, install the hooks:\n\n```\nbrunch install\n```\n\nCheck that everything is configured correctly:\n\n```\nbrunch doctor\n```\n\nThen activate the environment for the branch you are currently on:\n\n```\nbrunch activate\n```\n\nActivation is needed because installing the hooks does not itself cause a Git lifecycle event.\n\nAfter that, normal branch changes can drive the environment lifecycle.\n\nYou can inspect the current state with:\n\n```\nbrunch status\n```\n\nlist environments grouped by worktree:\n\n```\nbrunch list\n```\n\nand print the port assigned to the current worktree:\n\n```\nbrunch port\n```\n\nDocker Compose is the default environment manager, and Brunch also supports Podman Compose.\n\nBut the core idea is not:\n\n```\nbranch → Docker container\n```\n\nIt is:\n\n```\nbranch → development environment\n```\n\nFor example, Brunch can manage a local process:\n\n```\nmanager: local_process\ncommand: bin/dev\n```\n\nor project-specific commands:\n\n```\nmanager: command\n\ncommands:\n  create: bin/environment create\n  start: bin/environment start\n  stop: bin/environment stop\n  remove: bin/environment remove\n```\n\nContainers are convenient because they already provide strong isolation, but they are not required by the model.\n\nYou can.\n\nBrunch does not make anything possible that Docker Compose fundamentally could not do before.\n\nYou can manually create separate Compose projects, volumes, and ports.\n\nYou can also manually stop one environment and start another every time you change branches:\n\n```\ngit switch feature/login\ndocker compose -p feature-login up -d\n```\n\nThe interesting part is making the lifecycle follow Git automatically.\n\nI don't want changing context to mean:\n\n```\nswitch Git branch\nremember the previous environment\nstop it\nchoose the next project\nchoose its port\nstart it\n```\n\nI want:\n\n```\ngit switch feature/login\n```\n\nGit already knows that the context changed.\n\nBrunch reacts to that change.\n\nBranches can be observed through Git's reference machinery.\n\nWorktrees cannot.\n\nGit currently has no native lifecycle hooks for operations such as creating, removing, moving, locking, or unlocking a worktree.\n\nFor now, `git-hooks-ext` provides:\n\n```\nghe worktree ...\n```\n\nas a frontend for:\n\n```\ngit worktree ...\n```\n\nThe details of how that works are covered in my [git-hooks-ext article](https://dev.to/ciembor/git-hooks-ext-the-missing-git-callbacks-for-reference-transactions-2l3l).\n\nFor Brunch, the practical consequence is simple:\n\nIf you want automatic worktree lifecycle handling today, use:\n\n```\nghe worktree add -b feature/login ../feature-login\n```\n\ninstead of plain `git worktree add`.\n\nIf another tool creates the worktree directly, Brunch can still be activated manually inside it:\n\n```\ncd ../feature-login\nbrunch activate\n```\n\nThe wrapper works, but I would rather not need it.\n\nThe clean solution is for Git itself to expose worktree lifecycle hooks.\n\nThen IDEs, coding agents, Brunch, and any other tool could use ordinary:\n\n```\ngit worktree add ...\n```\n\nand Git could notify interested tools that the worktree was created.\n\nNo wrapper would be necessary.\n\nI have started an RFC discussion about adding worktree lifecycle hooks to Git.\n\nIf this would be useful in your workflow, please consider joining the discussion:\n\nThe most useful contribution is not just a `+1`.\n\nIf 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.\n\nFor this kind of change, it is useful to show why the functionality belongs in Git itself instead of another wrapper around `git worktree`.\n\nFor Brunch, native hooks would mean that IDEs and coding agents would not have to know that Brunch exists.\n\nThey could use Git normally.\n\nGit would report what happened.\n\nBrunch would react.\n\nThat is the main limitation I would like to remove from the current workflow.\n\nI reserved the RubyGems name for Brunch in 2018 because I wanted separate development databases for feature branches.\n\nEight years later, the idea became broader.\n\nA branch may imply not only different source code, but also a different database schema, data, services, volumes, and runtime configuration.\n\nAnd with worktrees and parallel coding agents, several of those environments may need to exist at the same time.\n\nThe difficult part was never creating another container.\n\nIt was connecting the lifecycle of the environment to the lifecycle of Git.\n\n`git-hooks-ext` provided that missing event layer.\n\nBrunch finally gives those events something useful to manage.\n\nEight years after reserving the name, I can switch a Git branch and have the development environment switch with it.", "url": "https://wpnews.pro/news/brunch-gem-isolated-development-environments-for-git-branches-and-worktrees", "canonical_source": "https://dev.to/ciembor/brunch-gem-isolated-development-environments-for-git-branches-and-worktrees-4n38", "published_at": "2026-10-07 16:11:54+00:00", "updated_at": "2026-10-07 16:17:29.320283+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents"], "entities": ["Brunch", "Git", "Docker Compose", "git-hooks-ext", "RubyGems", "Rails"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/brunch-gem-isolated-development-environments-for-git-branches-and-worktrees", "markdown": "https://wpnews.pro/news/brunch-gem-isolated-development-environments-for-git-branches-and-worktrees.md", "text": "https://wpnews.pro/news/brunch-gem-isolated-development-environments-for-git-branches-and-worktrees.txt", "jsonld": "https://wpnews.pro/news/brunch-gem-isolated-development-environments-for-git-branches-and-worktrees.jsonld"}}