How I Run Parallel AI Coding Sessions on the Same Laravel Project
Git worktrees + Docker Compose + one shell script = unlimited parallel branches, each with its own browser URL
I work solo on a Laravel monolith. My AI pair programmer (Claude Code) is fast, thorough, and relentless — but it can only work on one branch at a time. While it's deep in a feature implementation, I'm just... sitting there. Waiting. Reading the diff. Running the verification steps.
I wanted to launch a second session on a different issue. And a third. Each one visible in the browser. Each one isolated. Each one disposable.
The problem? Docker Compose is bound to one directory. One project. One set of ports. You can't just docker-compose up twice and hope for the best.
Here's how I solved it.
My setup: macOS, VS Code with the Claude Code extension, Docker Desktop, and a Laravel 12 app running in Docker Compose. Everything in this post assumes a similar stack, but the core ideas — parameterized ports, isolated databases, worktree-aware tooling — apply to any Dockerized web application.
The problem in one sentence
Docker Compose ties your localhost:8000 (or whatever your app port is) to a single project directory. If you want to work on two branches simultaneously and see both in the browser, you need two separate Docker stacks on different ports — and they can't collide.
Why not just use git stash?
Because stashing is sequential. I don't want to:
- Stash my work
- Switch branches
- Spin up the other feature
- Verify it
- Switch back
- Pop the stash
- Remember where I was
I want to open two VS Code windows, each on a different branch, each with its own localhost URL. Work on both. Alt-tab between them. No context switching tax.
The architecture: worktrees + parameterized Docker
The solution has two parts:
1. Git worktrees give you multiple checked-out copies of the same repo. They share the git object store (so they're lightweight — not full clones), but each has its own branch, working directory, and files.
~/dev/
├── my-project/ # Main repo (localhost:8000)
├── my-project-wt-export-button/ # Worktree 1 (localhost:8001)
└── my-project-wt-fix-sidebar/ # Worktree 2 (localhost:8002)
2. Parameterized Docker Compose ports let each directory run its own stack without port collisions.
Step 1: Make your ports configurable
Replace hardcoded ports in docker-compose.yml with environment variables:
services:
app:
ports:
- "${APP_PORT:-8000}:80"
mysql:
ports:
- "${DB_PORT:-3306}:3306"
mailpit:
ports:
- "${MAIL_HTTP_PORT:-8025}:8025"
- "${MAIL_SMTP_PORT:-1025}:1025"
The :-8000 syntax means "use 8000 if the variable isn't set." Your main repo works exactly as before — zero behavior change. But now each worktree can set its own ports in a local .env.
Step 2: Isolate session cookies
This one bit me during testing. I logged into localhost:8000, then localhost:8001, then went back to :8000 — and I was logged out.
Why? Browsers share cookies across ports on the same domain. Both apps set a cookie called laravel_session for localhost. Logging into one overwrites the other.
The fix: each worktree gets a unique session cookie name.
# In the worktree's .env:
SESSION_COOKIE=my_app_session_8001
Laravel reads SESSION_COOKIE from the environment. One line, problem solved.
Step 3: Automate everything with a script
I wrote a ~200 line bash script that handles the entire lifecycle:
# Create a parallel session
./scripts/parallel-session.sh create feature/export-button
# Output:
# Session ready!
# App: http://localhost:8001
# MySQL: localhost:3307
# Mailpit: http://localhost:8026
# List active sessions
./scripts/parallel-session.sh list
# Output:
# my-project-wt-export-button http://localhost:8001 [feature/export-button] (running)
# Destroy when done
./scripts/parallel-session.sh destroy feature/export-button
The create command:
- Runs
git worktree addto create the worktree (from thedevelopbranch) - Copies
.envfrom the main repo - Overwrites port variables with auto-calculated unique values
- Sets a unique
SESSION_COOKIE - Runs
docker-compose up -d
Port assignment is simple: session N gets base port + N. If you destroy session 1 and create a new one, it reuses the gap.
| Session | App | MySQL | Mailpit |
|-------------|--------|--------|---------|
| Main repo | :8000 | :3306 | :8025 |
| Worktree 1 | :8001 | :3307 | :8026 |
| Worktree 2 | :8002 | :3308 | :8027 |
Step 4: Seed independently
Each worktree gets its own MySQL volume. The database starts empty. After creating a session:
cd ~/dev/my-project-wt-export-button
docker-compose exec app php artisan migrate:fresh --seed
Timing matters here. If your Dockerfile uses a named volume for vendor/, it starts empty on first boot. Your entrypoint likely runs composer install to populate it. You need to wait for that to finish before running artisan commands — otherwise you'll get "class not found" errors because the packages aren't installed yet. My script waits for Apache to respond (which means the entrypoint completed) before attempting to seed.
This is actually a feature, not a bug. Each session has a clean database, seeded from scratch. No stale data from another branch's migrations. No "works on my machine because I manually ran that one migration."
The daily workflow
Starting a parallel session:
- Run the create script (30 seconds — Docker images are cached)
- Run
migrate:fresh --seed(10 seconds) - Open the worktree directory in VS Code
- Start working
While working:
- Each VS Code window is a completely independent project
- Each has its own
localhostURL for browser testing - Git works normally —
commit,push, create PRs/MRs - Tests target the correct containers automatically (Docker Compose resolves from the current directory)
Cleanup after merging:
Once you've merged the MR in GitLab, the cleanup flow is straightforward:
- Close the worktree's VS Code window — there's nothing left to do there.
- Switch to the main repo's VS Code window and run the destroy command:
./scripts/parallel-session.sh destroy feature/export-button
This stops the Docker containers, removes the database volume, deletes the worktree directory, and leaves you back on develop. From there you can sync your local branch with a quick git pull origin develop and delete the feature branch with git branch -d feature/export-button. The entire teardown takes about five seconds.
"But what about database conflicts?"
This was my biggest concern too. If session 1 adds column pdf_path and session 2 adds column export_format to the same table, what happens?
Nothing dramatic. Each session creates a migration file with a unique timestamp. When both branches merge:
- Both migration files coexist in
database/migrations/ php artisan migrateruns them in timestamp order- Both columns get added
The worktree databases are disposable scratch pads. You never merge database data — you merge migration files. The reconciliation happens in git, the same way it always does.
The only real conflict scenario: two branches modify the exact same column. That shows up as a git merge conflict in the PR, same as if you'd built the features sequentially.
Resource cost
Each parallel session runs its own MySQL, Redis, and app container. On my M-series Mac:
- ~250MB RAM per session
- ~500MB disk per MySQL volume
- Docker image layers are shared (cached), so no extra disk for the app image
I comfortably run 2-3 sessions alongside my main repo. Beyond that, my laptop would probably start complaining.
What I'd do differently
Cookie isolation should be the default. I wasted 20 minutes debugging mysterious logouts before realizing cookies were the issue. If you're building this, add SESSION_COOKIE to your .env.example from day one.
Named volumes are your friend. Docker Compose auto-prefixes volume names with the project name (derived from the directory name). Each worktree directory has a unique name, so volumes are automatically isolated. I didn't have to configure anything — it just worked.
Don't share the database. I briefly considered having all worktrees connect to the same MySQL instance (different databases). Bad idea. Different branches have different migrations. Isolation is the whole point.
Don't race your own entrypoint. My first attempt added a composer install step to the setup script — but the container's entrypoint was already running composer install to populate the empty vendor volume. Two concurrent composer installs on the same directory corrupted zip downloads and crashed the container. The fix was to let the entrypoint do its job and have the script wait for Apache to respond before proceeding.
Check your API key restrictions. If you use third-party APIs with domain or URL restrictions (Google Maps, Stripe, OAuth callbacks), they're probably locked to localhost:8000. Your worktree on localhost:8001 will get silent failures — a blank map, a rejected OAuth redirect, a 403 from a payment form. I got a console error on Google Maps because my API key's "Website restrictions" only listed :8000. The fix was trivial (add :8001/* and :8002/* to the allowed list), but the debugging wasn't obvious. Add your worktree ports to every restricted API key upfront.
Audit every git add -A in your tooling. If you symlink shared directories into worktrees (documentation, assets, anything gitignored), a blanket git add -A can pick them up in unexpected ways. I now require explicit file-by-file staging in worktree contexts. It's a minor inconvenience that eliminates a whole category of "why did that get committed?" moments.
Teach your AI agent about worktrees
This is the part nobody warns you about. Your Docker setup can be flawless, your ports perfectly isolated, your databases independently seeded — and your AI coding assistant will still trip over itself if it doesn't know it's running inside a worktree.
The first time I shipped code from a worktree session, the agent finished by cheerfully suggesting: "When you've merged, run /ship-consentio develop to sync up." That command switches to the develop branch — which is already checked out in the main repo. Git refuses to check out the same branch in two worktrees. The command fails immediately.
The fix was to teach the agent to detect its environment. If the working directory name contains consentio-wt-, the agent knows it's in a worktree session and adjusts its behavior accordingly. Instead of suggesting branch switches and local cleanup, it tells you to close the VS Code window and run the destroy command from the main repo. Same outcome, correct path to get there.
The second issue was subtler. I symlink the project's docs/ directory into each worktree so the agent can reference documentation during implementation. The directory is gitignored, so it should be invisible to version control. But git add -A — the lazy, catch-all staging command that every AI agent defaults to — doesn't always play nicely with symlinks in worktree contexts. The solution was to forbid git add -A entirely when working in a worktree and require the agent to stage files by name. It's a small constraint that prevents a class of confusing errors.
If you're building a similar setup with an AI coding tool, the lesson is this: the infrastructure is the easy part. The hard part is making sure your agent's playbook — its prompt templates, command files, or system instructions — includes conditional logic for the worktree context. Anywhere your prompts say "switch to develop," "sync the branch," or "stage all changes," you need a worktree-aware variant. Otherwise your carefully isolated sessions will work perfectly right up until the moment the AI tries to clean up after itself.
The result
I went from working on one issue at a time to running 2-3 parallel AI sessions. While Claude is writing tests for feature A, I'm reviewing the implementation plan for feature B. While waiting for feature B's CI pipeline, I'm verifying feature C in the browser.
My throughput roughly doubled. Not because the AI got faster, but because I stopped being the bottleneck.
The entire setup is:
- One
docker-compose.ymlchange (4 hardcoded ports to 4 env variables) - One
.env.exampleupdate (document the new variables) - One bash script (~200 lines)
- One line in
.gitignore(docker-compose.override.yml)
No external tools. No paid services. No complex orchestration. Just git, Docker, and a shell script.
Follow-up: The half I left out of my parallel worktrees post — how I populated each worktree with realistic data, and the staging comparison that caught my demo seeder quietly lying to me.
The code snippets in this post are simplified from a real production setup. Your ports, service names, and seeder commands will differ. The principles are universal for any Dockerized web app.