|
| 1 | +--- |
| 2 | +name: portless |
| 3 | +description: Set up and use portless for named local dev server URLs (e.g. https://myapp.localhost instead of http://localhost:3000). Use when integrating portless into a project, configuring dev server names, setting up the local proxy, working with .localhost domains, or troubleshooting port/proxy issues. |
| 4 | +--- |
| 5 | + |
| 6 | +# Portless |
| 7 | + |
| 8 | +Replace port numbers with stable, named .localhost URLs. For humans and agents. |
| 9 | + |
| 10 | +## Why portless |
| 11 | + |
| 12 | +- **Port conflicts**: `EADDRINUSE` when two projects default to the same port |
| 13 | +- **Memorizing ports**: which app is on 3001 vs 8080? |
| 14 | +- **Refreshing shows the wrong app**: stop one server, start another on the same port, stale tab shows wrong content |
| 15 | +- **Monorepo multiplier**: every problem scales with each service in the repo |
| 16 | +- **Agents test the wrong port**: AI agents guess or hardcode the wrong port |
| 17 | +- **Cookie/storage clashes**: cookies on `localhost` bleed across apps; localStorage lost when ports shift |
| 18 | +- **Hardcoded ports in config**: CORS allowlists, OAuth redirects, `.env` files break when ports change |
| 19 | +- **Sharing URLs with teammates**: "what port is that on?" becomes a Slack question |
| 20 | +- **Browser history is useless**: `localhost:3000` history is a mix of unrelated projects |
| 21 | + |
| 22 | +## Installation |
| 23 | + |
| 24 | +Install globally (recommended) or as a project dev dependency. Do NOT use `npx` or `pnpm dlx` for one-off execution. |
| 25 | + |
| 26 | +```bash |
| 27 | +# Global (available everywhere) |
| 28 | +npm install -g portless |
| 29 | + |
| 30 | +# Or per-project dev dependency |
| 31 | +npm install -D portless |
| 32 | +``` |
| 33 | + |
| 34 | +When installed per-project, invoke via package.json scripts or `npx portless` (since the package is local, npx will not download anything). |
| 35 | + |
| 36 | +## Quick Start |
| 37 | + |
| 38 | +```bash |
| 39 | +# Install globally (or add -D to a project) |
| 40 | +npm install -g portless |
| 41 | + |
| 42 | +# Run your app (auto-starts the HTTPS proxy on port 443) |
| 43 | +portless run next dev |
| 44 | +# -> https://<project>.localhost |
| 45 | + |
| 46 | +# Or with an explicit name |
| 47 | +portless myapp next dev |
| 48 | +# -> https://myapp.localhost |
| 49 | +``` |
| 50 | + |
| 51 | +The proxy auto-starts when you run an app. You can also start it explicitly with `portless proxy start`. |
| 52 | + |
| 53 | +## Integration Patterns |
| 54 | + |
| 55 | +### package.json scripts |
| 56 | + |
| 57 | +```json |
| 58 | +{ |
| 59 | + "scripts": { |
| 60 | + "dev": "portless run next dev" |
| 61 | + } |
| 62 | +} |
| 63 | +``` |
| 64 | + |
| 65 | +The proxy auto-starts when you run an app. Or start it explicitly: `portless proxy start`. |
| 66 | + |
| 67 | +### Multi-app setups with subdomains |
| 68 | + |
| 69 | +```bash |
| 70 | +portless myapp next dev # https://myapp.localhost |
| 71 | +portless api.myapp pnpm start # https://api.myapp.localhost |
| 72 | +portless docs.myapp next dev # https://docs.myapp.localhost |
| 73 | +``` |
| 74 | + |
| 75 | +By default, only explicitly registered subdomains are routed (strict mode). Start the proxy with `--wildcard` to allow any subdomain of a registered route to fall back to that app (e.g. `tenant1.myapp.localhost` routes to the `myapp` app). Exact matches always take priority over wildcards. |
| 76 | + |
| 77 | +### Git worktrees |
| 78 | + |
| 79 | +`portless run` automatically detects git worktrees. In a linked worktree, the branch name is prepended as a subdomain prefix so each worktree gets a unique URL: |
| 80 | + |
| 81 | +```bash |
| 82 | +# Main worktree (no prefix) |
| 83 | +portless run next dev # -> https://myapp.localhost |
| 84 | + |
| 85 | +# Linked worktree on branch "fix-ui" |
| 86 | +portless run next dev # -> https://fix-ui.myapp.localhost |
| 87 | +``` |
| 88 | + |
| 89 | +No config changes needed. Put `portless run` in `package.json` once and it works in all worktrees. |
| 90 | + |
| 91 | +### Bypassing portless |
| 92 | + |
| 93 | +Set `PORTLESS=0` to run the command directly without the proxy: |
| 94 | + |
| 95 | +```bash |
| 96 | +PORTLESS=0 pnpm dev # Bypasses proxy, uses default port |
| 97 | +``` |
| 98 | + |
| 99 | +## How It Works |
| 100 | + |
| 101 | +1. `portless proxy start` starts an HTTPS reverse proxy on port 443 as a background daemon. Auto-elevates with sudo on macOS/Linux; falls back to port 1355 if sudo is unavailable. Use `--no-tls` for plain HTTP on port 80. Configurable with `-p` / `--port` or the `PORTLESS_PORT` env var. The proxy also auto-starts when you run an app. |
| 102 | +2. `portless <name> <cmd>` assigns a random free port (4000-4999) via the `PORT` env var and registers the app with the proxy |
| 103 | +3. The browser hits `https://<name>.localhost`; the proxy forwards to the app's assigned port |
| 104 | + |
| 105 | +`.localhost` domains resolve to `127.0.0.1` natively in Chrome, Firefox, and Edge. Safari relies on the system DNS resolver, which may not handle `.localhost` subdomains on all configurations. Run `portless hosts sync` to add entries to `/etc/hosts` if needed. |
| 106 | + |
| 107 | +Most frameworks (Next.js, Express, Nuxt, etc.) respect the `PORT` env var automatically. For frameworks that ignore `PORT` (Vite, VitePlus, Astro, React Router, Angular, Expo, React Native), portless auto-injects the correct `--port` flag and, when needed, a matching `--host` CLI flag. |
| 108 | + |
| 109 | +### State directory |
| 110 | + |
| 111 | +Portless stores its state (routes, PID file, port file) in a directory that depends on the proxy port: |
| 112 | + |
| 113 | +- **Port < 1024** (sudo required): `/tmp/portless` (macOS/Linux only) |
| 114 | +- **Port >= 1024** (no sudo): `~/.portless` |
| 115 | +- **Windows**: Always `~/.portless` (no privileged port concept) |
| 116 | + |
| 117 | +Override with the `PORTLESS_STATE_DIR` environment variable. |
| 118 | + |
| 119 | +### Environment variables |
| 120 | + |
| 121 | +| Variable | Description | |
| 122 | +| --------------------- | --------------------------------------------------------------------- | |
| 123 | +| `PORTLESS_PORT` | Override the default proxy port (default: 443 with HTTPS, 80 without) | |
| 124 | +| `PORTLESS_APP_PORT` | Use a fixed port for the app (skip auto-assignment) | |
| 125 | +| `PORTLESS_HTTPS` | HTTPS on by default; set to `0` to disable (same as `--no-tls`) | |
| 126 | +| `PORTLESS_LAN` | Set to `1` to always enable LAN mode (auto-detects LAN IP) | |
| 127 | +| `PORTLESS_TLD` | Use a custom TLD instead of localhost (e.g. test) | |
| 128 | +| `PORTLESS_WILDCARD` | Set to `1` to allow unregistered subdomains to fall back to parent | |
| 129 | +| `PORTLESS_SYNC_HOSTS` | Set to `0` to disable auto-sync of /etc/hosts (on by default) | |
| 130 | +| `PORTLESS_STATE_DIR` | Override the state directory | |
| 131 | +| `PORTLESS=0` | Bypass the proxy, run the command directly | |
| 132 | + |
| 133 | +### HTTP/2 + HTTPS |
| 134 | + |
| 135 | +HTTPS with HTTP/2 is enabled by default (faster page loads for dev servers with many files). First run generates a local CA and adds it to the system trust store. After that, no prompts and no browser warnings. |
| 136 | + |
| 137 | +```bash |
| 138 | +portless proxy start --cert ./c.pem --key ./k.pem # Use custom certs |
| 139 | +portless proxy start --no-tls # Disable HTTPS (plain HTTP) |
| 140 | +portless trust # Add CA to trust store later |
| 141 | +``` |
| 142 | + |
| 143 | +On Linux, `portless trust` supports Debian/Ubuntu, Arch, Fedora/RHEL/CentOS, and openSUSE (via `update-ca-certificates` or `update-ca-trust`). On Windows, it uses `certutil` to add the CA to the system trust store. |
| 144 | + |
| 145 | +### LAN mode |
| 146 | + |
| 147 | +```bash |
| 148 | +portless proxy start --lan |
| 149 | +portless proxy start --lan --https |
| 150 | +portless proxy start --lan --ip 192.168.1.42 |
| 151 | +``` |
| 152 | + |
| 153 | +`--lan` advertises `<name>.local` hostnames over mDNS so any device on the same Wi-Fi can reach your apps. Portless auto-detects your LAN IP and follows network changes automatically, but you can pin a specific address with `--ip <address>` or the `PORTLESS_LAN_IP` environment variable. Set `PORTLESS_LAN=1` to default to LAN mode every time the proxy starts. |
| 154 | + |
| 155 | +Portless remembers LAN mode via `proxy.lan`, so if you stop a LAN proxy and start again, it stays in LAN mode. Other proxy settings still follow the current flags and env vars. Use `PORTLESS_LAN=0` for one start to switch back to `.localhost` mode. If a proxy is already running with different explicit LAN/TLS/TLD settings, portless warns and asks you to stop it first. |
| 156 | + |
| 157 | +LAN mode depends on the system mDNS helpers that portless launches: macOS includes `dns-sd`, while Linux uses `avahi-publish-address` from `avahi-utils` (install via `sudo apt install avahi-utils` or your distro’s tooling). |
| 158 | + |
| 159 | +- **Next.js**: add your `.local` hostnames to `allowedDevOrigins`: |
| 160 | + |
| 161 | + ```js |
| 162 | + // next.config.js |
| 163 | + module.exports = { |
| 164 | + allowedDevOrigins: ["myapp.local", "*.myapp.local"], |
| 165 | + }; |
| 166 | + ``` |
| 167 | + |
| 168 | +- **Expo / React Native**: portless always injects `--port`. React Native also gets `--host 127.0.0.1`. Expo gets `--host localhost` outside LAN mode, but in LAN mode portless leaves Metro on its default LAN host behavior instead of forcing `--host` or `HOST`. |
| 169 | + |
| 170 | +## CLI Reference |
| 171 | + |
| 172 | +| Command | Description | |
| 173 | +| -------------------------------------- | -------------------------------------------------------------- | |
| 174 | +| `portless run <cmd> [args...]` | Infer name from project, run through proxy (auto-starts) | |
| 175 | +| `portless run --name <name> <cmd>` | Override inferred base name (worktree prefix still applies) | |
| 176 | +| `portless <name> <cmd> [args...]` | Run app at `https://<name>.localhost` (auto-starts proxy) | |
| 177 | +| `portless get <name>` | Print URL for a service (for cross-service wiring) | |
| 178 | +| `portless get <name> --no-worktree` | Print URL without worktree prefix | |
| 179 | +| `portless list` | Show active routes | |
| 180 | +| `portless trust` | Add local CA to system trust store (for HTTPS) | |
| 181 | +| `portless clean` | Remove state, CA trust entry, and /etc/hosts block | |
| 182 | +| `portless proxy start` | Start HTTPS proxy as a daemon (port 443, auto-elevates) | |
| 183 | +| `portless proxy start --no-tls` | Start without HTTPS (plain HTTP on port 80) | |
| 184 | +| `portless proxy start --lan` | Start in LAN mode (mDNS `.local`, auto-follows LAN IP changes) | |
| 185 | +| `portless proxy start -p <number>` | Start the proxy on a custom port | |
| 186 | +| `portless proxy start --tld test` | Use .test instead of .localhost | |
| 187 | +| `portless proxy start --foreground` | Start the proxy in foreground (for debugging) | |
| 188 | +| `portless proxy start --wildcard` | Allow unregistered subdomains to fall back to parent route | |
| 189 | +| `portless proxy stop` | Stop the proxy | |
| 190 | +| `portless alias <name> <port>` | Register a static route (e.g. for Docker containers) | |
| 191 | +| `portless alias <name> <port> --force` | Overwrite an existing route | |
| 192 | +| `portless alias --remove <name>` | Remove a static route | |
| 193 | +| `portless hosts sync` | Add routes to /etc/hosts (fixes Safari) | |
| 194 | +| `portless hosts clean` | Remove portless entries from /etc/hosts | |
| 195 | +| `portless <name> --app-port <n> <cmd>` | Use a fixed port for the app instead of auto-assignment | |
| 196 | +| `portless <name> --force <cmd>` | Kill the existing process and take over its route | |
| 197 | +| `portless --name <name> <cmd>` | Force `<name>` as app name (bypasses subcommand dispatch) | |
| 198 | +| `portless <name> -- <cmd> [args...]` | Stop flag parsing; everything after `--` is passed to child | |
| 199 | +| `portless --help` / `-h` | Show help | |
| 200 | +| `portless run --help` | Show help for a subcommand (also: alias, hosts, clean) | |
| 201 | +| `portless --version` / `-v` | Show version | |
| 202 | + |
| 203 | +**Reserved names:** `run`, `get`, `alias`, `hosts`, `list`, `trust`, `clean`, and `proxy` are subcommands and cannot be used as app names directly. Use `portless run <cmd>` to infer the name, or `portless --name <name> <cmd>` to force any name including reserved ones. |
| 204 | + |
| 205 | +## Troubleshooting |
| 206 | + |
| 207 | +### Proxy not running |
| 208 | + |
| 209 | +The proxy auto-starts when you run an app with `portless <name> <cmd>`. If it doesn't start (e.g. port conflict), start it manually: |
| 210 | + |
| 211 | +```bash |
| 212 | +portless proxy start |
| 213 | +``` |
| 214 | + |
| 215 | +### Port already in use |
| 216 | + |
| 217 | +Another process is bound to the proxy port. Either stop it first, or use a different port: |
| 218 | + |
| 219 | +```bash |
| 220 | +portless proxy start -p 8080 |
| 221 | +``` |
| 222 | + |
| 223 | +### Framework not respecting PORT |
| 224 | + |
| 225 | +Portless auto-injects the right `--port` flag and, when needed, a matching `--host` flag for frameworks that ignore the `PORT` env var: **Vite**, **VitePlus** (`vp`), **Astro**, **React Router**, **Angular**, **Expo**, and **React Native**. SvelteKit uses Vite internally and is handled automatically. |
| 226 | + |
| 227 | +For other frameworks that don't read `PORT`, pass the port manually: |
| 228 | + |
| 229 | +- **Webpack Dev Server**: use `--port $PORT` |
| 230 | +- **Custom servers**: read `process.env.PORT` and listen on it |
| 231 | + |
| 232 | +### Permission errors |
| 233 | + |
| 234 | +The default ports (80 for HTTP, 443 for HTTPS) require `sudo` on macOS and Linux. Portless auto-elevates with sudo when needed. If sudo is unavailable, it falls back to port 1355 (no sudo needed). On Windows, no elevation is required. |
| 235 | + |
| 236 | +```bash |
| 237 | +portless proxy start --https # Auto-elevates with sudo for port 443 |
| 238 | +portless proxy start -p 1355 --https # No sudo needed (URLs include :1355) |
| 239 | +portless proxy stop # Stop (use sudo if started with sudo) |
| 240 | +``` |
| 241 | + |
| 242 | +### Safari can't find .localhost URLs |
| 243 | + |
| 244 | +Safari relies on the system DNS resolver for `.localhost` subdomains, which may not resolve them on all macOS configurations. Chrome, Firefox, and Edge have built-in handling. |
| 245 | + |
| 246 | +Fix: |
| 247 | + |
| 248 | +```bash |
| 249 | +portless hosts sync # Adds current routes to /etc/hosts |
| 250 | +portless hosts clean # Remove entries later |
| 251 | +``` |
| 252 | + |
| 253 | +Auto-syncs `/etc/hosts` for route hostnames by default. Set `PORTLESS_SYNC_HOSTS=0` to disable. |
| 254 | + |
| 255 | +### Browser shows certificate warning with --https |
| 256 | + |
| 257 | +The local CA may not be trusted yet. Run: |
| 258 | + |
| 259 | +```bash |
| 260 | +portless trust |
| 261 | +``` |
| 262 | + |
| 263 | +This adds the portless local CA to your system trust store. After that, restart the browser. |
| 264 | + |
| 265 | +### Remove portless from the machine |
| 266 | + |
| 267 | +```bash |
| 268 | +portless clean |
| 269 | +``` |
| 270 | + |
| 271 | +Stops the proxy if needed, removes the portless CA from the trust store (when portless added it), deletes known files under state directories, and removes the portless `/etc/hosts` block. May require `sudo` on macOS/Linux. |
| 272 | + |
| 273 | +### Proxy loop (508 Loop Detected) |
| 274 | + |
| 275 | +If your dev server proxies requests to another portless app (e.g. Vite proxying `/api` to `api.myapp.localhost`), the proxy must rewrite the `Host` header. Without this, portless routes the request back to the original app, creating an infinite loop. |
| 276 | + |
| 277 | +Fix: set `changeOrigin: true` in the proxy config (Vite, webpack-dev-server, etc.): |
| 278 | + |
| 279 | +```ts |
| 280 | +// vite.config.ts |
| 281 | +proxy: { |
| 282 | + "/api": { |
| 283 | + target: "https://api.myapp.localhost", |
| 284 | + changeOrigin: true, |
| 285 | + ws: true, |
| 286 | + }, |
| 287 | +} |
| 288 | +``` |
| 289 | + |
| 290 | +Portless automatically sets `NODE_EXTRA_CA_CERTS` in child processes so Node.js trusts the portless CA. If you run a separate Node.js process outside portless, point it at the CA manually: `NODE_EXTRA_CA_CERTS=/tmp/portless/ca.pem` (or `~/.portless/ca.pem` when the proxy runs on a non-privileged port like 1355). Alternatively, use `--no-tls` for plain HTTP. |
| 291 | + |
| 292 | +### Requirements |
| 293 | + |
| 294 | +- Node.js 20+ |
| 295 | +- macOS, Linux, or Windows |
| 296 | +- `openssl` (for `--https` cert generation; ships with macOS and most Linux distributions; on Windows, install via `winget install -e --id ShiningLight.OpenSSL.Dev` or use the copy bundled with Git for Windows) |
0 commit comments