Using Git Worktrees to Automate Development Environments

How I Built an Automated Git Worktree System That Ended Port Conflict Hell

A developer’s journey from manual environment switching nightmare to automated
multi-branch paradise

The 3 AM Panic Call

Picture this: You’re deep in the zone, building a complex user authentication
system for your web app. Three terminal windows open, Docker containers humming,
database migrations running smoothly. Then your phone buzzes.

“Hey, the payment system is completely broken in production. Can you look at it
RIGHT NOW?”

We’ve all been there. That sinking feeling when you realize you need to:

  1. Stop your current work
  2. Stash/commit half-finished code
  3. Switch branches
  4. Rebuild Docker containers
  5. Deal with port conflicts because your auth system is using port 3000
  6. Potentially lose your database state
  7. Fix the urgent issue
  8. Reverse all of the above to get back to your authentication work

By the time you’re back to your original task, you’ve lost 30 minutes and your
entire mental context. There had to be a better way.

The Aha Moment: Git Worktrees

I’d heard about Git worktrees before but never really got them. The
documentation made them sound like some advanced Git wizardry for kernel
developers. But when I finally wrapped my head around the concept, it was like
discovering fire:

Git worktrees let you check out multiple branches simultaneously in separate
directories.

Instead of switching branches in one folder, you can have:

my-project/              # Main branch
my-project-auth/         # Authentication feature branch
my-project-payments/     # Payment system fixes
my-project-integration/  # Integration testing branch

Each directory is a complete working copy, but they all share the same Git
repository. Mind = blown.

The Problem: It’s Not Just About Git

But here’s the catch - just having multiple directories doesn’t solve the real
problem. Each branch needs its own:

  • Database instance (can’t share data between features)
  • Port allocation (can’t run multiple servers on port 3000)
  • Environment configuration (different API keys, settings)
  • IDE setup (separate debug configurations)

Manually managing all of this for each worktree? That’s just trading one
nightmare for another.

The Solution: Automation, Automation, Automation

I decided to build a system that would:

  1. Automatically create Git worktrees with proper branch management
  2. Intelligently allocate ports to avoid conflicts
  3. Generate isolated environments for each worktree
  4. Set up IDE configurations automatically
  5. Handle Docker integration seamlessly

Here’s how I built it, step by step.

Step 1: The Port Allocation Formula

The first challenge was ports. If my main branch uses ports 3000-3010, what
ports should my feature branches use?

I came up with this simple formula:

SERVICE_PORT = BASE_PORT + (WORKTREE_INDEX * 10) + SERVICE_OFFSET

Here’s what this means:

  • Base Port: 40000 (nice high number, unlikely to conflict)
  • Worktree Index: 0 for main, 1 for first worktree, 2 for second, etc.
  • Service Offset: Different for each service (0=database, 1=web, 2=cache,
    etc.)

So for worktree index 1:

  • Database: 40000 + (1 × 10) + 0 = 40010
  • Web server: 40000 + (1 × 10) + 1 = 40011
  • Redis: 40000 + (1 × 10) + 2 = 40012

This gives each worktree a predictable block of 10 ports, completely eliminating
conflicts.

Step 2: Environment Templates

Next problem: each worktree needs its own environment configuration.

I created a .env.template file:

# .env.template
APP_NAME=MyApp-${WORKTREE_NAME}
DATABASE_URL=postgresql://user:pass@localhost:${POSTGRES_PORT}/myapp
REDIS_URL=redis://localhost:${REDIS_PORT}
WEB_PORT=${WEB_PORT}
WORKTREE_INDEX=${WORKTREE_INDEX}

My script processes this template, replacing variables with calculated values:

# Generated .env for feature-auth worktree
APP_NAME=MyApp-feature-auth
DATABASE_URL=postgresql://user:pass@localhost:40010/myapp
REDIS_URL=redis://localhost:40012
WEB_PORT=40011
WORKTREE_INDEX=1

Now each worktree gets a completely isolated environment with zero manual
configuration.

Step 3: Docker Integration

The Docker integration was trickier. I wanted to:

  • Share base images and volumes for efficiency
  • Keep database data separate per worktree
  • Avoid container name conflicts

Here’s my docker-compose approach:

services:
  web:
    ports:
      - "${WEB_PORT:-3000}:80"
    environment:
      - WORKTREE_INDEX=${WORKTREE_INDEX:-0}

  database:
    ports:
      - "${POSTGRES_PORT:-5432}:5432"
    volumes:
      - db-data-wt${WORKTREE_INDEX:-0}:/var/lib/postgresql/data

volumes:
  db-data-wt0: # Main branch database
  db-data-wt1: # Worktree 1 database
  # ... etc

The ${VAR:-default} syntax means the main branch still works with default
ports, but worktrees get their calculated ports.

Step 4: The Master Script

All of this logic went into a bash script called setup-worktree.sh. Here’s the
workflow:

#!/bin/bash
./setup-worktree.sh feature/user-authentication

The script:

  1. Validates the branch name and checks Git status
  2. Finds the next available worktree index by scanning existing worktrees
  3. Calculates all the ports using the formula
  4. Creates the Git worktree in ../feature-user-authentication/
  5. Generates the .env file from the template
  6. Sets up Docker containers with the new ports
  7. Configures IDE settings (PhpStorm run configurations)
  8. Tests that all services start successfully

Total time: about 30 seconds. What used to take me 10+ minutes of manual work.

Step 5: IDE Integration (The Secret Sauce)

Here’s where it gets really nice. The script also generates PhpStorm run
configurations:

<!-- Generated .idea/runConfigurations/Debug_WT1.xml -->
<component name="ProjectRunConfigurationManager">
  <configuration name="Debug WT1" type="PhpWebAppRunConfigurationType">
    <server name="localhost" host="localhost" port="40011" />
    <path_mappings>
      <mapping local-root="$PROJECT_DIR$" remote-root="/app" />
    </path_mappings>
  </configuration>
</component>

Now when I open PhpStorm in the worktree directory, I have ready-to-use debug
configurations with the correct ports. No manual setup required.

The Workflow Revolution

Here’s how my workflow changed:

Old Manual Process:

New Automated Process:

Before:

# Working on feature A
git stash
git checkout main
git checkout -b hotfix/payment-bug
# Stop containers, change ports, rebuild...
# 10 minutes later, finally working on the bug
# Fix the bug, commit, push
# Now reverse everything to get back to feature A...
# Another 10 minutes gone

After:

# Working on feature A, urgent bug comes in
./setup-worktree.sh hotfix/payment-bug
cd ../hotfix-payment-bug
# 30 seconds later, fully working environment
# Fix the bug, commit, push
cd ../feature-a
# Back to work immediately, nothing disrupted

The difference is night and day.

Real-World Results

After using this system for 3 months, here are the actual benefits I measured:

Time Savings:

  • Environment switching: 10 minutes → 30 seconds
  • Context recovery: 15 minutes → 0 minutes (no context lost)
  • Bug fix interruptions: ~25 minutes → ~2 minutes

Quality Improvements:

  • Zero production bugs from environment confusion
  • More willing to create branches for experiments
  • Better separation of concerns between features

Team Productivity:

  • No more “my branch conflicts with yours” conversations
  • Easy to review different branches simultaneously
  • Integration testing between branches becomes trivial

Lessons Learned (The Hard Way)

Mistake #1: Cache Permission Hell

My first version shared Docker volumes between worktrees to save disk space. Bad
idea. The cache directory permissions got completely messed up because different
containers were running with different user IDs.

Fix: Each worktree gets its own cache volume, but I optimized by sharing the
composer dependency cache (which is read-only after install).

Mistake #2: Port Formula Edge Cases

My initial port formula didn’t handle deleted worktrees well. If I deleted
worktree index 2, the system would try to reuse that index but the ports might
still be in use by lingering containers.

Fix: Added port availability checking before allocating:

check_port_available() {
    local port=$1
    if lsof -Pi :$port -sTCP:LISTEN -t >/dev/null 2>&1; then
        return 1  # Port in use
    fi
    return 0  # Port available
}

Mistake #3: Branch Name Assumptions

I initially assumed branch names would always be simple like feature/auth.
Then someone created feature/user-auth-with-2FA-and-social-login. The long
name broke my directory naming logic.

Fix: Added branch name sanitization:

sanitize_branch_name() {
    echo "$1" | sed 's/[^a-zA-Z0-9-]/-/g' | cut -c1-50
}

Mistake #4: Database State Confusion

With multiple databases running, I sometimes forgot which worktree had which
data state. Was the test user account in worktree 1 or 2?

Fix: Added database naming that includes the branch:

DATABASE_URL=postgresql://user:pass@localhost:40010/myapp_feature_auth

Now each worktree gets a uniquely named database, and I can see exactly what
data belongs where.

The Unexpected Benefit: Fearless Experimentation

The biggest benefit wasn’t what I expected. Having zero-cost branch creation
completely changed how I develop.

Before, creating a new branch meant:

  • “Is this experiment worth the setup time?”
  • “What if I mess up my main environment?”
  • “I’ll just hack this into my current branch for now…”

After:

  • “Let me spin up a quick worktree to try this idea”
  • “I can experiment without any risk”
  • “Why not create a separate branch for each approach?”

I’m creating 3x more branches now, but each one is focused and clean. My commit
history is much better, and code reviews are easier because each PR does exactly
one thing.

How You Can Build This Too

Want to implement something similar? Here’s my advice:

Start Simple

Don’t build everything at once. Start with just the port allocation formula and
manual environment creation. Get comfortable with Git worktrees first.

Pick Your Stack

My examples use:

  • Docker Compose for containerization
  • PostgreSQL for database
  • PHP/Symfony for the app
  • PhpStorm for IDE

But the concepts work with any stack. The port formula works everywhere, and
every IDE has some form of run configuration.

Focus on Your Pain Points

What takes you the longest when switching contexts? For me it was port conflicts
and Docker rebuilds. For you it might be database migrations or frontend build
processes.

Make It Reversible

Build in cleanup from day one. It’s easy to create worktrees; make sure you can
destroy them cleanly too.

Test with Your Team

What works for one developer might not work for a team. Test your automation
with colleagues before rolling it out.

The Code

I’ve open-sourced the core script and configuration templates. The main
components are:

  • setup-worktree.sh (232 lines of bash)
  • Makefile with worktree commands
  • .env.template for environment generation
  • Docker Compose integration
  • PhpStorm configuration generation

You can find it at [project repository] and adapt it for your stack.

What’s Next

This system has been running in production for our team of 8 developers for 6
months now. We’re working on:

  • Auto-cleanup for stale worktrees
  • Resource monitoring to track per-branch resource usage
  • Cloud integration for remote development environments
  • Team synchronization to share worktree configurations

Conclusion

Building this automated Git worktree system was one of those rare projects where
the solution exceeded my expectations. I thought I was just solving port
conflicts, but I ended up transforming my entire development workflow.

The key insight is that development environments are infrastructure, and
infrastructure should be automated. Just like we don’t manually provision
servers anymore, we shouldn’t manually manage development environments.

If you’re tired of the branch-switching dance, if you’ve ever lost work to
environment conflicts, or if you just want to experiment fearlessly, give this
approach a try. Your future self will thank you.


Have you built something similar? What’s your approach to managing multiple
development environments? I’d love to hear about it in the comments or reach out
on [Twitter/LinkedIn].

Related Reading:


Originally published on [Your Blog]. If this helped you, consider sharing it
with your team!