Say Goodbye to localhost:3000 — Meet Portless
If you’ve ever typed npm run dev, refreshed a browser tab, and found yourself staring at the wrong project — you already know why Portless exists.
Table of Contents
- The Problem in Plain Terms
- What Portless Actually Does
- HTTPS That Doesn’t Hurt
- Subdomains for Microservices
- The Killer Feature: Git Worktree Detection
- Custom TLDs
- Why This Matters for AI-Assisted Development
- Getting Started
Local development has a dirty little secret: port numbers are terrible identifiers. They collide, they’re impossible to memorize across a microservices stack, and they silently break things like OAuth redirect URIs, CORS allowlists, and browser cookies. Portless replaces all of that with stable, named .localhost URLs — and it takes about ten seconds to set up.
The Problem in Plain Terms
Most web frameworks default to the same handful of ports. Next.js grabs :3000. Vite takes :5173. Express picks :3000 again. The moment you’re running two services — or two branches of the same project — you’re playing port Tetris. You end up passing --port 3001, updating .env files, and telling your teammates on Slack which port to hit today.
It gets worse. Cookies scoped to localhost bleed across every app running on that host. localStorage vanishes when a port shifts. Your browser history for localhost:3000 becomes an archaeological dig through unrelated projects. And if you’re working with AI coding agents, they’ll happily hardcode the wrong port number the moment your setup drifts from the default.
These aren’t edge cases. They’re the everyday friction tax of modern local development.
What Portless Actually Does
Portless is a CLI tool that wraps your existing dev commands and routes traffic through a local reverse proxy. Instead of your framework binding to a well-known port, Portless auto-assigns a random port behind the scenes and maps it to a human-readable .localhost hostname.
The change to your workflow is a single prefix:
- "dev": "next dev" # http://localhost:3000
+ "dev": "portless myapp next dev" # https://myapp.localhost
Under the hood, Portless runs a reverse proxy on port 1355. Each app registers a route that maps its hostname to its randomly assigned port (drawn from the 4000–4999 range via the PORT environment variable). When your browser requests myapp.localhost, the proxy resolves it and forwards the traffic. The .localhost TLD auto-resolves to 127.0.0.1 in all modern browsers — no /etc/hosts editing required.
Browser (myapp.localhost:1355) → Proxy (port 1355) → App (random port)
Most frameworks (Next.js, Express, Nuxt, and others) respect the PORT environment variable natively. For frameworks that ignore it (Vite, Astro, React Router, Angular, Expo), Portless auto-injects the appropriate --port and --host CLI flags so everything just works.
HTTPS That Doesn’t Hurt
Local HTTPS has historically been a gauntlet of mkcert commands, certificate trust dialogs, and framework-specific flags. Portless collapses this to a single one-time step:
portless proxy start --https
On first run, it generates a local Certificate Authority and server certificates, then prompts once for sudo to trust the CA in your system keychain. After that, every .localhost URL serves over HTTP/2 with TLS — no browser warnings, no per-project configuration.
This isn’t just cosmetic. Browsers cap HTTP/1.1 at six concurrent connections per host, which creates real bottlenecks for dev servers that serve hundreds of unbundled modules. HTTP/2 multiplexes all of them over a single connection, making page loads noticeably faster during development.
If you prefer to bring your own certs (from mkcert or another tool), that’s supported too:
portless proxy start --cert ./cert.pem --key ./key.pem
Subdomains for Microservices
When you’re running multiple services (an API, a frontend, a docs site), Portless lets you organize them under a shared namespace using subdomains:
portless api.myapp pnpm start # → https://api.myapp.localhost
portless docs.myapp next dev # → https://docs.myapp.localhost
portless myapp next dev # → https://myapp.localhost
Each subdomain gets its own cookie scope and localStorage partition, eliminating the cross-contamination that plagues port-based setups. CORS allowlists and OAuth redirect URIs become stable strings you configure once and never touch again.
The Killer Feature: Git Worktree Detection
This is where Portless goes from “nice quality-of-life tool” to something that fundamentally changes how you work with branches.
If you use git worktree to check out multiple branches simultaneously, Portless auto-detects linked worktrees and prepends the branch name as a subdomain. Instead of naming the app explicitly, you use portless run, which infers the name from your project:
# Main worktree
portless run next dev # → https://myapp.localhost
# Linked worktree on branch "fix-ui"
portless run next dev # → https://fix-ui.myapp.localhost
There’s nothing to configure. You put portless run in your package.json once, and every worktree automatically gets a unique, deterministic URL. Port collisions, --force flags, and mental bookkeeping about which branch is on which port all go away. You can have five branches running side-by-side and switch between them by changing a subdomain in your address bar.
Custom TLDs
The .localhost TLD works out of the box in most browsers, but if you prefer something like .test, Portless supports that too:
sudo portless proxy start --https --tld test
portless myapp next dev # → https://myapp.test
When started with sudo, the proxy auto-syncs /etc/hosts for custom TLDs so they resolve correctly. The .test TLD is recommended since it’s reserved by IANA and won’t collide with real domains. Avoid .local (conflicts with mDNS/Bonjour) and .dev (owned by Google, enforces HSTS in browsers).
Why This Matters for AI-Assisted Development
There’s a less obvious but increasingly important benefit here: deterministic URLs make AI coding agents dramatically more reliable.
When an agent spins up a dev server and needs to verify its work in a browser, it typically guesses localhost:3000 — or whatever port the framework defaults to. In a monorepo, or when another service is already occupying that port, the agent either hits the wrong app or fails entirely. A stable, named URL like https://myapp.localhost removes that guesswork entirely. The URL is the same every time, regardless of what else is running on the machine.
Getting Started
Portless requires Node.js 20+ and runs on macOS, Linux, and Windows. Installation is a single global install:
npm install -g portless
Then wrap your dev command and go:
portless proxy start --https # One-time setup
portless myapp next dev # That's it
Update your package.json scripts and the change propagates to every developer (and every agent) on the team:
{
"scripts": {
"dev": "portless myapp next dev"
}
}
No configuration files, no daemon management, no Docker networking. Just a prefix that eliminates an entire category of local development friction.
Portless is open-source under Vercel Labs. Check out the documentation at port1355.dev and the source on GitHub.
lsof -i :3000 | kill again.”-Rushi