portree automatically manages multiple dev servers per git worktree — with automatic port allocation, environment variable injection, and *.localhost subdomain routing via reverse proxy.
Japanese version: README.ja.md
- Multi-service — Define frontend, backend, and any number of services per worktree
- Automatic port allocation — Hash-based port assignment (FNV32) with per-service ranges; no port conflicts across worktrees
- Subdomain reverse proxy — Access any worktree via
branch-name.localhost:<port>(no/etc/hostsediting required) - HTTPS proxy — Auto-generated certificates or custom cert/key for local HTTPS (Secure Cookies, Service Workers, etc.)
- Environment variable injection —
$PORT,$PT_BRANCH,$PT_BACKEND_URL, etc. are injected automatically - TUI dashboard — Interactive terminal UI to start, stop, restart, and monitor all services
- Process lifecycle — Graceful shutdown (SIGTERM → SIGKILL), log files, stale PID cleanup
- Per-worktree overrides — Customize commands, ports, and env vars per branch
- AI agent friendly —
portree ls --jsonincludesurl,direct_url, andproxy_runningfields for automatic endpoint discovery
# Homebrew
brew install fairy-pitta/tap/portree
# Go install
go install github.com/fairy-pitta/portree@latest
# Or build from source
git clone https://github.com/fairy-pitta/portree.git
cd portree
make buildcd your-project
portree init
# Creates .portree.toml in the repo root
# Adds .portree/ to .gitignore.portree.toml belongs in version control. .portree/ does not — it holds
runtime state, service logs, and the generated development CA private key.
Edit .portree.toml to match your project:
[services.frontend]
command = "pnpm run dev"
dir = "frontend"
port_range = { min = 3100, max = 3199 }
proxy_port = 3000
[services.backend]
command = "source .venv/bin/activate && python manage.py runserver 0.0.0.0:$PORT"
dir = "backend"
port_range = { min = 8100, max = 8199 }
proxy_port = 8000
[env]
NODE_ENV = "development"portree up # Start all services for the current worktree
portree up --all # Start all services for ALL worktreesup also starts the reverse proxy in the background and prints the URLs, so a
single command leaves you with addresses that answer:
✓ 2 services started for main
✓ Proxy running (http, pid 41233)
http://main.localhost:3000 → frontend
http://main.localhost:8000 → backend
Pass --no-proxy to start services only.
portree proxy status # Is it running, on which ports, under which scheme
portree proxy start --detach # Background, waits until it is actually serving
portree proxy start # Foreground, Ctrl+C to stop
portree proxy start --https # HTTPS with auto-generated certificates
portree proxy stopportree open # Opens http://main.localhost:3000
portree open --service backend # Opens http://main.localhost:8000open refuses to launch a browser when the proxy or the service is not
running, rather than leaving you on a connection error page.
| Command | Description |
|---|---|
portree init |
Create a .portree.toml configuration file |
portree up |
Start services for the current worktree, plus the proxy |
portree up --all |
Start services for all worktrees |
portree up --service |
Start a specific service only |
portree up --no-proxy |
Start services without starting the proxy |
portree down |
Stop services for the current worktree |
portree down --all |
Stop services for all worktrees |
portree ls |
List all worktrees, services, ports, status, and PIDs |
portree dash |
Open the interactive TUI dashboard |
portree proxy start |
Start the reverse proxy (foreground) |
portree proxy start --detach |
Start the reverse proxy in the background |
portree proxy start --https |
Start the reverse proxy with HTTPS (auto-generated certs) |
portree proxy status |
Report whether the proxy is running, and where |
portree proxy stop |
Stop the reverse proxy |
portree trust |
Install the CA certificate into the system trust store |
portree open |
Open the current worktree in a browser |
portree doctor |
Run diagnostic checks on config and ports |
portree version |
Print version information |
The .portree.toml file lives at the root of your git repository.
Which service portree open opens when --service is not given.
default_service = "frontend"Optional. When unset, the alphabetically first service is used — which for a
frontend + backend pair means the backend, so it is worth setting. portree init writes it for you.
Define one or more services. Each worktree will run all defined services.
| Field | Type | Required | Description |
|---|---|---|---|
command |
string | yes | Shell command to start the service |
dir |
string | no | Working directory relative to worktree root (default: root) |
port_range |
{min, max} |
yes | Port allocation range for this service |
proxy_port |
int | yes | Port the reverse proxy listens on for this service |
[services.frontend]
command = "pnpm run dev"
dir = "frontend"
port_range = { min = 3100, max = 3199 }
proxy_port = 3000Important
Make your command bind the allocated $PORT. portree injects the
allocated port as the PORT environment variable, but your service must
actually listen on it — otherwise it will start on its own default port and
portree will report it as running on a port nothing is listening on.
The reliable approach is to have your service read $PORT itself:
- Vite: set
server.portinvite.config.ts, e.g.server: { port: Number(process.env.PORT) || 5173 }, or runcommand = "npx vite --port $PORT". - Next.js:
command = "next dev -p $PORT". - Most frameworks honor
PORTout of the box (Rails, Django via0.0.0.0:$PORT, etc.).
pnpm caveat: command = "pnpm run dev -- --port $PORT" does not work.
pnpm inserts its own -- separator, producing vite ... -- --port 3193, and
Vite treats everything after -- as positional args — so --port is silently
ignored and Vite falls back to 5173. Use npx vite --port $PORT, or read
PORT inside vite.config.ts as shown above. (See
#9.)
Global environment variables injected into all services.
[env]
NODE_ENV = "development"
DATABASE_URL = "postgres://localhost/mydb"Per-worktree overrides. You can customize the command, fix a specific port, or add extra environment variables.
[worktrees.main]
services.frontend.port = 3100 # Fixed port for main branch
[worktrees."feature/auth"]
services.backend.command = "python manage.py runserver --settings=myapp.auth 0.0.0.0:$PORT"
services.backend.env = { DEBUG = "1" }portree automatically injects the following environment variables into every service process:
| Variable | Example | Description |
|---|---|---|
PORT |
3117 |
Allocated port for this service |
PT_BRANCH |
feature/auth |
Current branch name |
PT_BRANCH_SLUG |
feature-auth |
URL-safe slug of the branch name |
PT_SERVICE |
frontend |
Name of the current service |
PT_<SERVICE>_PORT |
PT_FRONTEND_PORT=3117 |
Port of each sibling service |
PT_<SERVICE>_URL |
PT_BACKEND_URL=http://feature-auth.localhost:8000 |
Proxy URL of each sibling service |
This allows services to discover each other automatically:
// next.config.js
module.exports = {
async rewrites() {
return [
{
source: "/api/:path*",
destination: `${process.env.PT_BACKEND_URL}/api/:path*`,
},
];
},
};┌─────────────────────────────────────────────────────────────┐
│ git repository │
│ │
│ main worktree feature/auth worktree │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ frontend :3100│ │ frontend :3117│ │
│ │ backend :8100│ │ backend :8104│ │
│ └───────────────┘ └───────────────┘ │
│ │ │ │
└─────────┼──────────────────────┼────────────────────────────┘
│ │
┌─────▼──────────────────────▼─────┐
│ portree reverse proxy │
│ │
│ :3000 ← *.localhost:3000 │
│ :8000 ← *.localhost:8000 │
└──────────────────────────────────┘
│ │
▼ ▼
main.localhost:3000 feature-auth.localhost:3000
main.localhost:8000 feature-auth.localhost:8000
- Port allocation — Each service gets a port via
FNV32(branch:service) % range. Stable across restarts. - Process management — Services run as child processes with process groups. Logs go to
.portree/logs/. - Reverse proxy — One HTTP listener per
proxy_port. Routes based onHostheader subdomain. *.localhost— Per RFC 6761, modern browsers resolve*.localhostto127.0.0.1automatically.
Launch with portree dash:
╭─ portree dashboard ──────────────────────────────────────────╮
│ │
│ WORKTREE SERVICE PORT STATUS PID │
│ ──────────────────────────────────────────────────────────── │
│ ▸ main frontend 3100 ● running 12345 │
│ main backend 8100 ● running 12346 │
│ feature/auth frontend 3117 ○ stopped — │
│ feature/auth backend 8104 ○ stopped — │
│ │
│ Proxy: ● running (:3000, :8000) │
│ │
│ [s] start [x] stop [r] restart [o] open in browser │
│ [a] start all [X] stop all [p] toggle proxy │
│ [l] view logs [q] quit │
╰───────────────────────────────────────────────────────────────╯
Key bindings:
| Key | Action |
|---|---|
j/k |
Move cursor down/up |
s |
Start selected service |
x |
Stop selected service |
r |
Restart selected service |
o |
Open in browser |
a |
Start all services |
X |
Stop all services |
p |
Toggle proxy |
l |
View log file path |
q |
Quit |
# You're working on a monorepo with frontend + backend
cd my-project
# Initialize portree
portree init
# Edit .portree.toml to define your services...
# Create a feature branch worktree
git worktree add ../my-project-feature-auth feature/auth
# Start services on your current branch
portree up
# Starting frontend (port 3100) for main ...
# Starting backend (port 8100) for main ...
# ✓ 2 services started for main
# Start services on ALL worktrees at once
portree up --all
# ✓ 4 services started
# Check status
portree ls
# WORKTREE SERVICE PORT STATUS PID
# main frontend 3100 running 12345
# main backend 8100 running 12346
# feature/auth frontend 3117 running 12347
# feature/auth backend 8104 running 12348
# JSON output (great for AI agents and scripts)
portree ls --json
# [{"worktree":"main","service":"frontend","port":3100,"status":"running","pid":12345,
# "url":"http://main.localhost:3000","direct_url":"http://localhost:3100",
# "proxy_running":true}, ...]
# "url" always reflects the configured proxy URL; "proxy_running" tells you
# whether anything is currently listening on it.
# Start the proxy
portree proxy start
# Access:
# http://main.localhost:3000 → frontend (main)
# http://main.localhost:8000 → backend (main)
# http://feature-auth.localhost:3000 → frontend (feature/auth)
# http://feature-auth.localhost:8000 → backend (feature/auth)
# Or start with HTTPS (for Secure Cookies, Service Workers, etc.)
portree proxy start --https
# Auto-generates certificates in .portree/certs/
# Access via https://main.localhost:3000
# Trust the CA to remove browser warnings
portree trust
# Open in browser
portree open
# Opening http://main.localhost:3000 ...
# Or use the TUI
portree dash
# When done
portree down --all
# ✓ 4 services stoppedportree supports shell completion for bash, zsh, fish, and PowerShell.
bash:
source <(portree completion bash)
# Or for persistent use:
portree completion bash > /etc/bash_completion.d/portreezsh:
portree completion zsh > "${fpath[1]}/_portree"
# You may need to start a new shell for this to take effect.fish:
portree completion fish | source
# Or for persistent use:
portree completion fish > ~/.config/fish/completions/portree.fishPowerShell:
portree completion powershell | Out-String | Invoke-Expression
# Or for persistent use:
portree completion powershell > portree.ps1
# and add ". portree.ps1" to your PowerShell profile.- Check the log file at
.portree/logs/<branch-slug>.<service>.logfor error output. - Verify the
commandin.portree.tomlruns correctly when executed manually. - Ensure the working
direxists relative to the worktree root. portree reports a missing directory by name before starting, andportree doctorlists every service whosediris absent.
- Run
portree doctorto check for port conflicts. - If a port is already in use, portree uses linear probing to find the next available port in the range.
- If the entire range is exhausted, widen the
port_rangein.portree.toml.
- Run
portree doctorto detect stale PIDs in the state file. - Use
portree down --allto clean up and stop all services. - If a process was killed externally,
portree lswill show it asstoppedautomatically.
- Ensure the proxy is running with
portree proxy start. - Verify your browser resolves
*.localhost— modern browsers do this per RFC 6761. - Check that the target service is actually running with
portree ls. - The proxy routes based on the
Hostheader subdomain, so access viahttp://<branch-slug>.localhost:<proxy_port>.
- Auto-generated certificates are stored in
.portree/certs/when usingportree proxy start --https. - Run
portree trustto install the CA certificate into your system trust store and eliminate browser warnings. - To use custom certificates, pass
portree proxy start --cert <path> --key <path>(both flags are required together). - To verify with curl:
curl --cacert .portree/certs/ca.crt https://main.localhost:3000.
| Platform | Status | Notes |
|---|---|---|
| macOS | Fully supported | Primary development platform |
| Linux | Fully supported | Tested on Ubuntu, Debian, Fedora |
| Windows | Experimental | Basic functionality works; file locking uses alternative implementation. Please report issues. |
Modern browsers (Chrome, Firefox, Edge, Safari) resolve *.localhost to 127.0.0.1 per RFC 6761. No /etc/hosts editing or DNS configuration is needed.
portree uses linear probing — if the hash-derived port is already taken, it tries the next port in the range until it finds a free one.
Yes. portree up starts your services with allocated ports. You can access them directly at localhost:<port>. The proxy is optional.
Service logs are written to .portree/logs/<branch-slug>.<service>.log in the main worktree's root.
Runtime state (PIDs, port assignments) is stored in .portree/state.json with file-level locking for concurrent access safety.
Yes, use [worktrees."branch-name"] overrides in .portree.toml:
[worktrees."feature/auth"]
services.backend.command = "python manage.py runserver --settings=auth 0.0.0.0:$PORT"
services.backend.env = { DEBUG = "1" }portree/
├── main.go # Entry point
├── cmd/ # CLI commands (cobra)
│ ├── root.go # Root command + repo/config detection
│ ├── init.go # portree init
│ ├── up.go # portree up
│ ├── down.go # portree down
│ ├── ls.go # portree ls
│ ├── dash.go # portree dash
│ ├── proxy.go # portree proxy start|stop
│ ├── trust.go # portree trust
│ ├── open.go # portree open
│ └── version.go # portree version
├── internal/
│ ├── cert/cert.go # CA + server certificate auto-generation
│ ├── config/config.go # .portree.toml loading & validation
│ ├── git/
│ │ ├── repo.go # Repo root / common dir detection
│ │ └── worktree.go # Worktree listing & branch slugs
│ ├── state/store.go # JSON state persistence with flock
│ ├── port/
│ │ ├── allocator.go # FNV32 hash-based port allocation
│ │ └── registry.go # Port assignment management
│ ├── process/
│ │ ├── runner.go # Single process lifecycle
│ │ └── manager.go # Multi-service orchestration
│ ├── proxy/
│ │ ├── resolver.go # Slug + port → backend resolution
│ │ └── server.go # HTTP/HTTPS reverse proxy
│ ├── browser/open.go # OS-aware browser opening
│ └── tui/ # Bubble Tea TUI dashboard
│ ├── app.go # Top-level model
│ ├── dashboard.go # Table rendering
│ ├── keys.go # Key bindings
│ ├── messages.go # Custom messages
│ └── styles.go # Lip Gloss styles
├── Makefile
├── .goreleaser.yaml
└── .github/workflows/
├── ci.yaml
└── release.yaml
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing) - Commit your changes (
git commit -m 'feat: add amazing feature') - Push to the branch (
git push origin feature/amazing) - Open a Pull Request
# Development
make build # Build binary
make test # Run tests with race detector
make lint # Run golangci-lint
make all # fmt + vet + lint + test + buildMIT License. See LICENSE for details.



