I run multiple coding agents in parallel, each in its own git worktree with its own dev server. When your apps reference each other by port, you can't just let the framework pick whatever's available.
The Problem
A typical monorepo has apps that reference each other:
Spin up a second worktree and frameworks like Next.js will auto-pick a new port. But your web app doesn't know the API moved to :4001. It's still pointing to :4000 for API calls and internal URLs. Everything breaks silently.
The Fix
Each worktree gets a base port. All app ports derive from it.
Here's how it works, step by step.
1. Pick a Base Port
Set up a worktree and pass in a base port:
# create a worktree git worktree add ../my-feature feature-branch cd ../my-feature # install deps bun install # assign ports BASE_PORT=5200 bash scripts/setup-dev-ports.sh
setup-dev-ports.sh derives each port from the base:
#!/bin/bash if [ -n "$BASE_PORT" ]; then WEB_PORT=$((BASE_PORT)) # base + 0 API_PORT=$((BASE_PORT + 1)) # base + 1 ADMIN_PORT=$((BASE_PORT + 2)) # base + 2 else WEB_PORT=3000 API_PORT=4000 ADMIN_PORT=6060 fi
BASE_PORT=5200 gives you 5200, 5201, 5202. Second worktree, pick a different number:
git worktree add ../my-other-feature other-branch cd ../my-other-feature bun install BASE_PORT=5210 bash scripts/setup-dev-ports.sh turbo dev --parallel
Two isolated dev environments, side by side.
2. Write .env Files
The script writes .env.development.local files (highest priority in Next.js dev mode, gitignored by default):
cat > apps/web/.env.development.local <<EOF WEB_PORT=$WEB_PORT NEXT_PUBLIC_API_HOST=localhost:$API_PORT EOF cat > apps/api/.env.development.local <<EOF API_PORT=$API_PORT NEXT_PUBLIC_WEB_DOMAIN=localhost:$WEB_PORT EOF
This handles both the listening port and the sibling URLs. Every app in the worktree now points to its siblings' correct ports.
3. Read Ports at Dev Time
Each app's dev script sources the env file:
{
"scripts": {
"dev": "sh -c '[ -f .env.development.local ] && . ./.env.development.local; next dev --port ${WEB_PORT:-3000}'"
}
}The :-3000 fallback means bun dev still works without the setup script.
Automating It with Conductor
I use Conductor to run a bunch of coding agents in parallel. I don't want to think about port numbers every time I spin one up.
A conductor.json at the repo root handles it:
{
"scripts": {
"setup": "bun install && bash scripts/setup-dev-ports.sh",
"run": "turbo dev --parallel"
}
}setupruns once on workspace init. Installs deps and configures ports.runstarts the dev servers.
Conductor exposes a CONDUCTOR_PORT to each workspace, a base port from a reserved block of 10. So the setup script becomes:
#!/bin/bash if [ -n "$CONDUCTOR_PORT" ]; then WEB_PORT=$((CONDUCTOR_PORT)) # base + 0 API_PORT=$((CONDUCTOR_PORT + 1)) # base + 1 ADMIN_PORT=$((CONDUCTOR_PORT + 2)) # base + 2 else WEB_PORT=3000 API_PORT=4000 ADMIN_PORT=6060 fi cat > apps/web/.env.development.local <<EOF WEB_PORT=$WEB_PORT NEXT_PUBLIC_API_HOST=localhost:$API_PORT EOF cat > apps/api/.env.development.local <<EOF API_PORT=$API_PORT NEXT_PUBLIC_WEB_DOMAIN=localhost:$WEB_PORT EOF
No manual port picking. Conductor guarantees non-overlapping ranges.
Now I can spin up multiple workspaces and test them in parallel. localhost:5200 shows one branch, localhost:5210 shows another, and each one talks to its own API.
Why Not Spotlight Testing?
Conductor also has Spotlight Testing, which syncs worktree changes back to your repo root so you can reuse your existing dev server.
It works great, but Spotlight allows you to run one feature at a time. With multiple agents going, I needed each workspace fully isolated with its own servers.