The problem
- One compose file.
- Two worktrees.
- Same host ports.
- Same database.
- The second fails.
$ docker compose up -d Container myapp-billing-postgres-1 ErrorError response from daemon: Bind for 0.0.0.0:5434 failed: port is already allocated $ grep DATABASE_URL .envDATABASE_URL=postgresql://app:app@ localhost:5434/appdocker compose -p only renames the project. You would still pick free ports, edit every worktree's .env, copy compose files across and tear it all down by hand. wtc does each of those for you. A URL with the port inside it, like that DATABASE_URL, takes one line of config.
The whole loop in 46 seconds
Four agents, four worktrees, four stacks on their own ports. Compare them live, promote the winner into main, then clean up.
Same file, new ports
wtc reads every host port written as ${VAR:-default} and gives each worktree its own. Pick a service, a worktree and a stride, or type your own port, and check the arithmetic.
"${FRONTEND_PORT:-5173}:5173"20000 + 5173 + 1 × 1 = 25174FRONTEND_PORT=25174Defaults that sit close together, like mocks on 8081 to 8086, can collide between worktrees at stride 1. wtc warns when it sees that and recommends the smallest portStride that keeps them apart.
From worktree to stack
First, write host ports as ${VAR:-default}.
services: postgres: image: postgres:16 ports: - " ${POSTGRES_PORT:-5434}:5432" backend: build: ./backend ports: - " ${BACKEND_PORT:-8000}:8000" frontend: build: ./frontend ports: - " ${FRONTEND_PORT:-5173}:5173"Main still gets 5434, 8000 and 5173. A hardcoded port gets a warning with the fix:
Service "nginx" uses a raw port mapping (8080:80). To enable port isolation, change it to: "${NGINX_PORT:-8080}:80"Then add a worktree and start it.
$ git worktree add ../myapp-auth feature-auth$ wtc start feature-authwtc copies the env file from main, writes the worktree's ports into it and runs compose up. Keys main already has, like POSTGRES_PORT, are rewritten in place; new ones go in a marked block. The copy is made fresh on every start, so edits made only in the worktree's env file are replaced: keep lasting values in main's, or in envOverrides when they come from a port.
NODE_ENV=development # --- wtc port overrides ---COMPOSE_PROJECT_NAME=myapp-wt-1-feature-authPOSTGRES_PORT=25435BACKEND_PORT=28001FRONTEND_PORT=25174# --- end wtc ---With COMPOSE_PROJECT_NAME in .env, a plain docker compose ps, your Makefile and your own scripts in the worktree reach this stack without wtc. Compose reads no other env file on its own, so with a custom envFile like .env.development, add --env-file .env.development.
Now every stack runs side by side.
$ wtc listOpen 25174 and 25175 in two tabs and compare the branches while both run.
┌───────┬──────────────┬────────┬────────────────────────┬────────────────────────────────────────────────────────────┐
│ Index │ Branch │ Status │ URL │ Ports │
├───────┼──────────────┼────────┼────────────────────────┼────────────────────────────────────────────────────────────┤
│ - │ main │ - │ - │ postgres:5434 backend:8000 frontend:5173 │
├───────┼──────────────┼────────┼────────────────────────┼────────────────────────────────────────────────────────────┤
│ 1 │ feature-auth │ up │ http://localhost:25174 │ POSTGRES_PORT=25435 BACKEND_PORT=28001 FRONTEND_PORT=25174 │
├───────┼──────────────┼────────┼────────────────────────┼────────────────────────────────────────────────────────────┤
│ 2 │ fix-billing │ up │ http://localhost:25175 │ POSTGRES_PORT=25436 BACKEND_PORT=28002 FRONTEND_PORT=25175 │
├───────┼──────────────┼────────┼────────────────────────┼────────────────────────────────────────────────────────────┤
│ 3 │ refactor-api │ down │ http://localhost:25176 │ POSTGRES_PORT=25437 BACKEND_PORT=28003 FRONTEND_PORT=25176 │
└───────┴──────────────┴────────┴────────────────────────┴────────────────────────────────────────────────────────────┘Scroll the table sideways for the URLs and ports.
Remove one, and the rest keep their numbers.
$ wtc stop 2$ git worktree remove ../myapp-billing$ wtc listWorktree 3 is still worktree 3, on the same ports, with the same containers and volumes. wtc records each worktree's index and Compose project name the first time it sees it, and the ports follow from the index. Delete a worktree without stopping it and wtc list shows it as (removed) until wtc prune tears its stack down.
┌───────┬──────────────┬────────┬────────────────────────┬────────────────────────────────────────────────────────────┐
│ Index │ Branch │ Status │ URL │ Ports │
├───────┼──────────────┼────────┼────────────────────────┼────────────────────────────────────────────────────────────┤
│ - │ main │ - │ - │ postgres:5434 backend:8000 frontend:5173 │
├───────┼──────────────┼────────┼────────────────────────┼────────────────────────────────────────────────────────────┤
│ 1 │ feature-auth │ up │ http://localhost:25174 │ POSTGRES_PORT=25435 BACKEND_PORT=28001 FRONTEND_PORT=25174 │
├───────┼──────────────┼────────┼────────────────────────┼────────────────────────────────────────────────────────────┤
│ 3 │ refactor-api │ down │ http://localhost:25176 │ POSTGRES_PORT=25437 BACKEND_PORT=28003 FRONTEND_PORT=25176 │
└───────┴──────────────┴────────┴────────────────────────┴────────────────────────────────────────────────────────────┘Agents run their own stacks
wtc mcp is a built-in MCP server. Claude Code, Codex, Cursor or any MCP client can start, list, promote and clean up worktree stacks itself, while you compare the results on their own ports.
Register it once, for all your projects:
$ claude mcp add --scope user wtc -- wtc mcpOr share it with the team in the project's .mcp.json:
{ "mcpServers": { "wtc": { "command": "wtc", "args": ["mcp"] } }}Add it to Codex's config file:
[mcp_servers.wtc]command = "wtc"args = ["mcp"]Add it to the project's Cursor config:
{ "mcpServers": { "wtc": { "command": "wtc", "args": ["mcp"] } }}Any other MCP client: run wtc mcp as a stdio server. This needs the global install, so wtc is on your PATH.
A tool for each command
wtc_start, wtc_stop, wtc_restart, wtc_list, wtc_promote, wtc_clean, wtc_prune and wtc_doctor take the same worktree indices and branch names as the commands. Like the commands, wtc_clean and wtc_prune delete volumes, and wtc_clean keeps worktrees with uncommitted work.
Or teach it the workflow
SKILL.md is an agent skill that covers the commands, port allocation, config and the MCP server in one file. Install it for Claude Code with one command, or hand it to any agent that reads skills.
$ wtc skill --install# writes ~/.claude/skills/worktree-compose/SKILL.mdCommands
Config when you need it
Most repos need none. wtc reads docker-compose.yml, follows its include: files and syncs every Dockerfile a service builds. It finds the compose command on its own, for Docker Compose v2, for Podman through podman compose or podman-compose, and for Docker-compatible engines such as OrbStack and Colima. When a project needs more, it goes in .wtcrc.json at the repo root, or a "wtc" key in package.json.
In a monorepo the env files live next to each app. Name the file and the workspaces, and every worktree gets its own copies, injected, with the root always included:
$ wtc start --env .env.development --workspace server --workspace homepage{ "envFile": ".env.development", "workspaces": ["server", "homepage"], "envOverrides": { "DATABASE_URL": "postgresql://app:app@localhost: ${POSTGRES_PORT}/app" }, "workspaceEnvOverrides": { "homepage": { "VITE_API_URL": "http://localhost: ${BACKEND_PORT}" } }, "sync": [".generated/prisma-client", "local-certs/"], "portStride": 100, "composeCommand": "podman compose"}envFile- The env file to copy from main and inject into, at the root and in each selected workspace. Defaults to
.env. Per run:--env. workspaces- Monorepo packages that also get their env file copied and injected, by package name, directory or path. Per run:
--workspace, repeatable. envOverrides- Extra variables for the root env file, with
${VAR}filled in from that worktree's ports. A URL with a port in it, likeDATABASE_URL, goes here. Keys that already exist are rewritten in place; new ones go in the wtc block. workspaceEnvOverrides- The same, for one workspace's env file.
sync- Gitignored or generated files Docker needs, copied from main on every start: generated clients, local certificates.
portStride- Spacing between worktrees' ports, 1 by default. Raise it when default ports sit close together.
composeCommand- The compose command to run when you want to choose it yourself, such as
podman compose. TheWTC_COMPOSE_COMMANDvariable does the same from the shell.