Persistent local port management for macOS, with a daemon-backed dashboard for inspecting listeners, reassigning ports, and enforcing the port layout you actually want.
- Background daemon with a localhost-only dashboard and global
portctlCLI - Live port discovery on macOS using
lsofandps, refreshed every second - Grouped process cards with CPU, memory, uptime, tags, and multi-port visibility
- One-click process control for kill, suspend, resume, restart, and port reassignment
- Port reservations that automatically move known apps back to their preferred ports
- Blocked ports and pinned ports for keeping your local environment predictable
- Hidden, renamed, tagged, and manually ungrouped cards for personal workflow control
- Log viewer for processes started or restarted through
portctl - Card and table views, drag reordering, theme settings, and persistent local config
curl -fsSL https://raw.githubusercontent.com/august-andersen/portctl/main/install.sh | bash
portctl start
portctl openThe install script clones the repo into ~/.portctl/app, installs dependencies, builds the client and server bundles, and links a global portctl command.
| Command | Description |
|---|---|
portctl start |
Start the background daemon. If it is already running, print the dashboard URL and exit. |
portctl stop |
Stop the daemon gracefully. |
portctl restart |
Stop and start the daemon again. |
portctl status |
Print whether the daemon is running, its PID, uptime, and dashboard URL. |
portctl open |
Open the dashboard in the default macOS browser. |
portctl uninstall |
Remove the daemon, runtime files, logs, config, and CLI symlink after confirmation. |
portctl stores all persistent state in ~/.portctl/config.json. The file is created automatically on first start and backed up to ~/.portctl/.config.json.bak before every write.
Example structure:
{
"version": 1,
"settings": {
"dashboardPort": 47777,
"pollingInterval": 1000,
"defaultView": "card",
"theme": "dark",
"cardClickBehavior": "openBrowser",
"logBufferSize": 10000
},
"reservations": [],
"blockedPorts": [],
"pinnedPorts": [],
"tags": {},
"cardOrder": [],
"customRestartCommands": {}
}Reservations are the feature that makes portctl feel smart rather than reactive.
- Start a service normally.
- Move it to the port you actually want.
- Open the card menu and choose
Reserve port. - Keep the auto-detected matcher, or switch to process name, working directory, or regex.
- The next time that service appears on the wrong port, portctl will move it back.
If a non-reserved process is occupying the reserved port, portctl will clear the way before migrating the reserved process. If two reservations conflict with each other, portctl raises a visible error instead of guessing.
Requirements:
- macOS
- Node.js 20+
lsof,ps, and other standard macOS process tools
Install and run locally:
npm install
npm run build
node bin/portctl.js startDuring UI development, run the server and Vite side by side:
npm run devThat starts:
- the API server on the configured dashboard port, default
47777 - the Vite dev server on
http://127.0.0.1:5174
Project layout:
bin/ CLI entry point
client/ React dashboard
server/ daemon, discovery, actions, routes, config
shared/ shared TypeScript contracts
docs/ preview assets for GitHub
portctl uninstallManual cleanup if needed:
rm -rf ~/.portctl
rm -f /usr/local/bin/portctl /opt/homebrew/bin/portctl ~/.local/bin/portctlMIT. See LICENSE.
