Tired of Error: listen EADDRINUSE: address already in use :::3000?
PortMux is a developer-focused CLI for running and coordinating background processes. It solves the chronic problem of port conflicts in projects with multiple services, especially when working across several Git branches.
It reserves ports for your process groups before they start and ties each reservation to a Git worktree. When another worktree already owns a port, PortMux fails fast and tells you which one; the select command will stop the old worktree and start the new one so you can reuse the same ports safely.
While tools like pm2 or systemd are excellent for managing production services, PortMux is purpose-built for the development inner loop, prioritizing simplicity and eliminating common frustrations.
Japanese write-up: https://zenn.dev/yuta_takahashi/articles/introduce-portmux-cli
PortMux is built on a few core principles to streamline the developer experience:
-
Frictionless Parallelism with Git Worktrees: The core feature. PortMux maps process state to individual Git worktrees. Clone your repository into multiple worktrees (
git worktree add ...), andportmux selectwill stop whichever worktree is holding the ports before starting the one you choose—no manual cleanup required. -
Predictable by Default: By reserving ports before launching your commands, PortMux fails fast and tells you exactly which port is unavailable. This avoids the pain of one service in a group failing mid-startup because another service took its port.
-
Developer-First Simplicity: PortMux is a lightweight, daemon-less CLI. It manages state in a transparent file-based system within
~/.config/portmux/, giving you full control and visibility without a persistent background process to manage.
- Git worktree–aware process tracking; if another worktree already holds a port, starts fail fast and
portmux selectcan hand off the running group for you - Group-oriented process management with shared start/stop/restart flows
- Port reservation to avoid collisions before booting services
- Environment templating with
${VAR}expansion from config orprocess.env - Persistent state for PIDs, ports, and logs under
~/.config/portmux/ - Git-aware group resolution so you can run commands from any subdirectory
- Node.js 20+
| OS | Status | Notes |
|---|---|---|
| macOS | Supported | Actively maintained. |
| Linux | Not yet verified | Expected to work on modern distributions; please report compatibility findings. |
| Windows | Experimental | Uses wmic for process inspection; unverified and likely incomplete. |
- Global install:
pnpm add -g @portmux/cliornpm install -g @portmux/cli(or run ad hoc vianpx @portmux/cli) - From source:
pnpm installpnpm buildpnpm dev:cli -- --helpto run the built CLI locally
portmux initin your project root to generateportmux.config.jsonand register the repo in~/.config/portmux/config.json(use--forceto overwrite). If your repo already includesportmux.config.json(e.g., after cloning), runportmux sync --allfor monorepos or any project with multiple groups to register everything without prompts.- Edit the generated config (or review the existing one) and keep your group definitions up to date. Example:
{ "$schema": "https://raw.githubusercontent.com/YTakahashii/portmux/main/packages/cli/schemas/portmux.config.schema.json", "groups": { "app": { "description": "Demo group", "commands": [ { "name": "web", "command": "pnpm dev", "ports": [3000], "cwd": "./apps/web", "env": { "API_URL": "http://localhost:3000" } } ] } } } - Ensure the repository is registered in the global config with
portmux sync(prefer--allfor monorepos or when multiple groups exist). - Start everything with
portmux start. - Inspect running processes with
portmux ps. - Follow logs with
portmux logs <group> <process>and stop withportmux stop [group] [process].
PortMux's core value shines when you're working on multiple features at once. Here’s how you can use it with Git worktrees to switch between two versions of your app without fighting over ports.
The portmux select command is the smoothest way to switch contexts. It automatically stops processes running in the current worktree and starts them in the one you select.
-
Create two worktrees for different features:
# Create a worktree for feature-a git worktree add ../project-feature-a feature-a # Create another for feature-b git worktree add ../project-feature-b feature-b
-
Start working on
feature-a: You can be in any directory of your project.portmux selectwill find all associated worktrees.# Select the first worktree to start its processes portmux select # ? Select a repository: (Use arrow keys) # > project (feature-a) [/path/to/project-feature-a] # project (main) [/path/to/project] # project (feature-b) [/path/to/project-feature-b] # After selecting, it starts automatically # ▶ Starting group 'app' in worktree 'project (feature-a)'... # ▶ web (PID: 12345) is running...
-
Switch to
feature-bwithout changing directories: When you need to work on the other feature, just runselectagain. You don't even need tocd. PortMux handles stopping the old environment and starting the new one.# Still in your original directory portmux select # ? Select a repository: # project (feature-a) [/path/to/project-feature-a] # project (main) [/path/to/project] # > project (feature-b) [/path/to/project-feature-b] # It gracefully stops the 'feature-a' processes before starting 'feature-b' # ▶ Stopping processes for group 'app' in worktree 'project (feature-a)'... # ▶ Starting group 'app' in worktree 'project (feature-b)'... # ▶ web (PID: 54321) is running...
The following examples assume you have a group named app with a process named web.
# Start a configured group (auto-resolves when only one exists)
portmux start
# Start a specific group or process
portmux start app
portmux start app web
# Restart or stop
portmux restart app web
portmux stop app
# When multiple groups are running and you want to stop everything at once
portmux stop --all
# Show running state for all worktrees
portmux ps
# Tail logs (default 50 lines, follow)
portmux logs app web
portmux logs app web -n 200 --no-follow
# Choose a registered project and start from the global config
portmux select --all
# Register an existing project config into the global config
portmux sync
portmux sync --allportmux init [--force]: Interactive setup forportmux.config.jsonand global registration.portmux start [group] [process] [--all]: Start processes with port reservation and env substitution.--allstarts every group defined in the project config for the current worktree.portmux restart [group] [process] [--all]: Stop then start using the same resolution rules asstart.--allrestarts every running process in the current worktree.portmux stop [group] [process] [--all] [-t, --timeout <ms>]: Stop processes; errors when multiple groups are running unless--all, and--timeoutcontrols the wait before SIGKILL (default: 3000 ms).portmux ps: List group, process name, status, and PID.portmux select [--all]: Pick a registered repository and runstart;--allincludes entries outside Git worktrees.portmux sync [--all] [--group <name>] [--name <alias>] [--dry-run] [--force] [--prune]: Register the current project config in~/.config/portmux/config.json. When multiple groups exist you must pass--group <name>or--all; otherwise the command exits with an error.portmux logs <group> <process> [-n <lines>] [--no-follow] [-t]: Tail logs with optional timestamps.
- PortMux executes
commandvalues via your shell (e.g., to allow pipes/redirects). Use configs you trust and review sharedportmux.config.jsonfiles before running them.
$schema(optional): Point tonode_modules/@portmux/cli/schemas/portmux.config.schema.jsonfor editor IntelliSense.runner.mode(optional): Currently onlybackgroundis supported.groups(required): Object keyed by group name.description: Group description.commands: Array of processes.name/command(required): Process name and shell command.ports(optional): Port numbers to reserve; accepts integers or${VAR}placeholders resolved fromenvthenprocess.env(must resolve to positive integers).cwd(optional): Working directory for the process. Defaults to the project root.env(optional): String map of environment variables;${VAR}expands fromenvthenprocess.env(missing values warn and resolve to an empty string).
repositories: Map keyed by repository alias.path: Absolute path to the project root.group: Group name inportmux.config.json.
logs(optional): Global log settings applied for this user.maxBytes: Per-user log cap in bytes (defaults to 10MB when omitted).disabled: When true, suppresses all process log output.
portmux initappends the current project;start/restart/selectuse this mapping for resolution.portmux syncis the quickest way to register a repo that already ships withportmux.config.json(e.g., after cloning). When only one group exists it registers that group by default; otherwise pass--group <name>or--all(and--pruneto drop stale entries that no longer exist on disk).- Repository paths are written with the home directory shortened to
~by default; absolute paths remain supported and are expanded at runtime.
- Repository paths are written with the home directory shortened to
- When
start/restartomit the group, resolution checks the global config and Git worktree first. - If auto-resolution fails, PortMux searches upward from the current directory for
portmux.config.jsonand uses the first group definition.
stdout/stderrare written to~/.config/portmux/logs/; view withportmux logs.- Process state, PIDs, and reserved ports persist in
~/.config/portmux/for reuse bypsandlogs. - Log files are automatically trimmed to the newest content when they exceed 10MB by default; set a per-user cap via
logs.maxBytesin~/.config/portmux/config.json. - Trimming runs at process start and when listing processes (
portmux ps), keeping only the tail within the configured limit. - Disable logging globally by adding
"logs": { "disabled": true }to~/.config/portmux/config.json(stdout/stderr are ignored when disabled). - Log cleanup:
portmux stopremoves the associated log file, andportmux psprunes log files not referenced by any recorded process state. No separate prune command is required.
Run portmux sync --all (or --group <name> for a single group) in the repo so it registers in ~/.config/portmux/config.json.
The repository is not registered. Run portmux sync --all in the worktree you want to use.
Run portmux sync --all in that worktree to register it.
Use portmux select to hand off to the active worktree, or portmux stop --all to clear running groups, then retry.
Ensure portmux.config.json exists in the project root (re-run portmux init if needed).
To remove PortMux from your system, first uninstall the global package:
- Using pnpm:
pnpm remove -g @portmux/cli
- Using npm:
npm uninstall -g @portmux/cli
This will remove the portmux command. To completely remove all associated data (including repository history, logs, and process state), delete the configuration directory:
rm -rf ~/.config/portmuxWarning: This action is irreversible and will delete all PortMux settings and log files.
See CONTRIBUTING.md for development setup, verification commands, and contribution guidelines.
