Labsco
vercel-labs logo

portless

10,000

by vercel · part of vercel-labs/portless

Named .localhost URLs for dev servers, eliminating port conflicts and hardcoded port numbers. Assigns random free ports (4000–4999) automatically and routes named subdomains (e.g. myapp.localhost:1355 ) to the correct app via a reverse proxy on port 1355 Auto-detects git worktrees and prepends branch names as subdomain prefixes so each worktree gets a unique URL without configuration changes Supports HTTP/2 with HTTPS via auto-generated local CA certificates; includes CLI commands for proxy...

🔥🔥🔥✓ VerifiedFreeNeeds API keys
🧰 Not standalone. This skill ships with vercel-labs/portless and only works together with that tool — install the tool first, then add this skill.

Named .localhost URLs for dev servers, eliminating port conflicts and hardcoded port numbers. Assigns random free ports (4000–4999) automatically and routes named subdomains (e.g. myapp.localhost:1355 ) to the correct app via a reverse proxy on port 1355 Auto-detects git worktrees and prepends branch names as subdomain prefixes so each worktree gets a unique URL without configuration changes Supports HTTP/2 with HTTPS via auto-generated local CA certificates; includes CLI commands for proxy...

Inspect the full instructions your agent will receiveExpand

This is the exact playbook injected into your agent when the skill activates — shown here so you can audit it before installing. You don't need to read it to use the skill.

by vercel

Named .localhost URLs for dev servers, eliminating port conflicts and hardcoded port numbers. Assigns random free ports (4000–4999) automatically and routes named subdomains (e.g. myapp.localhost:1355 ) to the correct app via a reverse proxy on port 1355 Auto-detects git worktrees and prepends branch names as subdomain prefixes so each worktree gets a unique URL without configuration changes Supports HTTP/2 with HTTPS via auto-generated local CA certificates; includes CLI commands for proxy... npx skills add https://github.com/vercel-labs/portless --skill portless Download ZIPGitHub10k

Portless

Replace port numbers with stable, named .localhost URLs. For humans and agents.

Why portless

  • Port conflicts: EADDRINUSE when two projects default to the same port

  • Memorizing ports: which app is on 3001 vs 8080?

  • Refreshing shows the wrong app: stop one server, start another on the same port, stale tab shows wrong content

  • Monorepo multiplier: every problem scales with each service in the repo

  • Agents test the wrong port: AI agents guess or hardcode the wrong port

  • Cookie/storage clashes: cookies on localhost bleed across apps; localStorage lost when ports shift

  • Hardcoded ports in config: CORS allowlists, OAuth redirects, .env files break when ports change

  • Sharing URLs with teammates: "what port is that on?" becomes a Slack question

  • Browser history is useless: localhost:3000 history is a mix of unrelated projects

Integration Patterns

Zero-config (recommended)

Bare portless works out of the box. It runs the "dev" script from package.json through the proxy, inferring the app name from the package name, git root, or directory:

Copy & paste — that's it
portless # -> runs "dev" script, https:// .localhost
pnpm dev # -> works without portless, plain "next dev"

Use an optional portless.json to override defaults (name, script, port):

Copy & paste — that's it
{ "name": "myapp" }
Copy & paste — that's it
portless # -> runs "dev" script, https://myapp.localhost

Monorepo

One portless.json at the repo root. Portless discovers packages from pnpm-workspace.yaml, or the "workspaces" field in package.json (npm, yarn, bun):

Copy & paste — that's it
{
 "apps": {
 "apps/web": { "name": "myapp" },
 "apps/api": { "name": "api.myapp" }
 }
}
Copy & paste — that's it
portless # from repo root: start all packages with a "dev" script
cd apps/web && portless # start just one package
portless --script start # run "start" instead of "dev"

The apps map is optional and only provides name overrides. Unlisted packages auto-discover with inferred names.

Without an apps map, hostnames follow <package>.<project>.localhost. The project name comes from the most common npm scope (e.g. @myorg/web and @myorg/api produce myorg), falling back to the workspace root directory name. If a package's short name matches the project name, it uses the bare <project>.localhost.

Turborepo

For turborepo projects, use portless as the dev script with the real command in a separate script:

Copy & paste — that's it
{
 "scripts": { "dev": "portless", "dev:app": "next dev" },
 "portless": { "name": "myapp", "script": "dev:app" }
}

pnpm dev runs turbo, which runs portless in each package. Portless detects the package manager and runs pnpm run dev:app through the proxy.

package.json scripts

You can still use portless directly in scripts:

Copy & paste — that's it
{
 "scripts": {
 "dev": "portless run next dev"
 }
}

The proxy auto-starts when you run an app. Or start it explicitly: portless proxy start.

Multi-app setups with subdomains

Copy & paste — that's it
portless myapp next dev # https://myapp.localhost
portless api.myapp pnpm start # https://api.myapp.localhost
portless docs.myapp next dev # https://docs.myapp.localhost

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.

Git worktrees

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:

Copy & paste — that's it
# Main worktree (no prefix)
portless run next dev # -> https://myapp.localhost

# Linked worktree on branch "fix-ui"
portless run next dev # -> https://fix-ui.myapp.localhost

No config changes needed. Put portless run in package.json once and it works in all worktrees.

Bypassing portless

Set PORTLESS=0 to run the command directly without the proxy:

Copy & paste — that's it
PORTLESS=0 pnpm dev # Bypasses proxy, uses default port

How It Works

  • 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.

  • portless <name> <cmd> assigns a random free port (4000-4999) via the PORT env var and registers the app with the proxy

  • The browser hits https://<name>.localhost; the proxy forwards to the app's assigned port

.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.

Use portless proxy start --tld localhost --tld test to serve the same app names under multiple TLDs from one proxy. PORTLESS_URL uses the first configured TLD. PORTLESS_TLD accepts the same comma separated list format, e.g. PORTLESS_TLD=localhost,test.

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.

State directory

Portless stores its state (routes, PID file, port file) in ~/.portless. Override with the PORTLESS_STATE_DIR environment variable.

Environment variables

Variable Description PORTLESS_PORT Override the default proxy port (default: 443 with HTTPS, 80 without) PORTLESS_APP_PORT Use a fixed port for the app (skip auto-assignment) PORTLESS_HTTPS HTTPS on by default; set to 0 to disable (same as --no-tls) PORTLESS_LAN Set to 1 to always enable LAN mode (auto-detects LAN IP) PORTLESS_LAN_IP Pin a specific LAN IP for LAN mode PORTLESS_TLD Use one or more TLDs (e.g. localhost,test) PORTLESS_WILDCARD Set to 1 to allow unregistered subdomains to fall back to parent PORTLESS_SYNC_HOSTS Set to 0 to disable auto-sync of /etc/hosts (on by default) PORTLESS_TAILSCALE Set to 1 to share apps on your Tailscale network (same as --tailscale) PORTLESS_FUNNEL Set to 1 to share apps publicly via Tailscale Funnel (same as --funnel) PORTLESS_NGROK Set to 1 to share apps publicly via ngrok (same as --ngrok) PORTLESS_STATE_DIR Override the state directory PORTLESS=0 Bypass the proxy, run the command directly

HTTP/2 + HTTPS

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.

Copy & paste — that's it
portless proxy start --cert ./c.pem --key ./k.pem # Use custom certs
portless proxy start --no-tls # Disable HTTPS (plain HTTP)
portless trust # Add CA to trust store later

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.

LAN mode

Copy & paste — that's it
portless proxy start --lan
portless proxy start --lan --https
portless proxy start --lan --ip 192.168.1.42

--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.

Portless remembers LAN mode via proxy.lan, so if you stop a LAN proxy and start again, it stays in LAN mode. All proxy settings (port, TLS, TLDs, LAN) are persisted and reused on auto-start unless overridden by explicit flags or 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.

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).

Next.js: add your .local hostnames to allowedDevOrigins:

Copy & paste — that's it
// next.config.js
module.exports = {
 allowedDevOrigins: ["myapp.local", "*.myapp.local"],
};

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.

Tailscale sharing

Share dev servers with teammates on your Tailscale network using --tailscale, or expose to the public internet with --funnel:

Copy & paste — that's it
portless myapp --tailscale next dev
# -> https://myapp.localhost (local)
# -> https://devbox.yourteam.ts.net (tailnet)

portless myapp --funnel next dev
# -> https://myapp.localhost (local)
# -> https://devbox.yourteam.ts.net (public internet)

Tailscale HTTPS certificates must be enabled before --tailscale or --funnel can register HTTPS URLs. Funnel must also be enabled for the tailnet and node before --funnel can register the public URL. If either setting is missing, portless exits before starting the child process.

Each --tailscale app is root-mounted on its own Tailscale HTTPS port (443, then 8443, 8444, etc.) so no framework basePath configuration is needed. Set PORTLESS_TAILSCALE=1 to share every app by default. portless list shows both local and tailnet URLs. Tailscale serve registrations are cleaned up when the app exits. Requires tailscale CLI installed and connected, with Tailscale HTTPS certificates enabled.

ngrok sharing

Expose a dev server to the public internet with ngrok using --ngrok:

Copy & paste — that's it
portless myapp --ngrok next dev
# -> https://myapp.localhost (local)
# -> https://abc123.ngrok.app (public internet)

Set PORTLESS_NGROK=1 to enable ngrok by default when portless runs an app. portless list shows both local and ngrok URLs. The ngrok tunnel is cleaned up when the app exits. Requires the ngrok CLI to be installed and authenticated with ngrok config add-authtoken <token>.

OS startup service

Use the service command when users want the proxy to start automatically after reboot:

Copy & paste — that's it
portless service install
portless service install --lan
portless service install --wildcard
PORTLESS_STATE_DIR=~/.portless-lan PORTLESS_LAN=1 portless service install
portless service status
portless service uninstall

The service uses portless defaults unless install options or PORTLESS_* environment variables are provided: HTTPS on port 443 with .localhost names. service install accepts proxy options including --port, --no-tls, --lan, --ip, --tld, --wildcard, --cert, and --key. Use --state-dir <path> or PORTLESS_STATE_DIR=<path> to choose where service state and logs are written.

The chosen service configuration is written into launchd, systemd, or Task Scheduler and reused after reboot. portless service status reports the installed port, HTTPS mode, TLDs, LAN mode, wildcard mode, and state directory. macOS and Linux install a root-owned service so port 443 can bind at boot. Windows installs a Task Scheduler startup task that runs as SYSTEM. Installation and removal may require administrator privileges. portless clean automatically removes the service.

CLI Reference

Command Description portless Run dev script through proxy portless From monorepo root: run all workspace packages portless --script <name> Run a specific package.json script (default: dev) portless run [cmd] [args...] Infer name from project, run through proxy (auto-starts) portless run --name <name> <cmd> Override inferred base name (worktree prefix still applies) portless <name> <cmd> [args...] Run app at https://<name>.localhost (auto-starts proxy) portless get <name> Print URL for a service (for cross-service wiring) portless get <name> --no-worktree Print URL without worktree prefix portless list Show active routes portless doctor Check proxy, routes, DNS, CA trust, and LAN prerequisites portless trust Add local CA to system trust store (for HTTPS) portless clean Remove state, CA trust entry, and /etc/hosts block portless prune Kill orphaned dev servers from crashed sessions portless prune --force Kill orphans with SIGKILL instead of SIGTERM portless proxy start Start HTTPS proxy as a daemon (port 443, auto-elevates) portless proxy start --no-tls Start without HTTPS (plain HTTP on port 80) portless proxy start --lan Start in LAN mode (mDNS .local, auto-follows LAN IP changes) portless proxy start -p <number> Start the proxy on a custom port portless proxy start --tld test Use .test instead of .localhost portless proxy start --tld localhost --tld test Serve both TLDs from one proxy portless proxy start --foreground Start the proxy in foreground (for debugging) portless proxy start --wildcard Allow unregistered subdomains to fall back to parent route portless proxy stop Stop the proxy portless service install Start the HTTPS proxy when the OS starts portless service install --lan Start the service in LAN mode portless service install --wildcard Persist wildcard routing in the startup service portless service status Show service and proxy status portless service uninstall Remove the startup service portless alias <name> <port> Register a static route (e.g. for Docker containers) portless alias <name> <port> --force Overwrite an existing route portless alias --remove <name> Remove a static route portless hosts sync Add routes to /etc/hosts (fixes Safari) portless hosts clean Remove portless entries from /etc/hosts portless <name> --app-port <n> <cmd> Use a fixed port for the app instead of auto-assignment portless <name> --tailscale <cmd> Share the app on your Tailscale network (tailnet) portless <name> --funnel <cmd> Share the app publicly via Tailscale Funnel portless <name> --ngrok <cmd> Share the app publicly via ngrok portless <name> --force <cmd> Kill the existing process and take over its route portless --name <name> <cmd> Force <name> as app name (bypasses subcommand dispatch) portless <name> -- <cmd> [args...] Stop flag parsing; everything after -- is passed to child portless --help / -h Show help portless run --help Show help for a subcommand (also: alias, hosts, clean) portless --version / -v Show version

Reserved names: run, get, alias, hosts, list, doctor, trust, clean, prune, proxy, and service 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.

portless.json

Optional config file. Portless looks for it in the current directory.

Field Type Default Description name string inferred from package.json Base app name (worktree prefix still applies) script string "dev" Name of a package.json script to run appPort number auto-assigned Fixed port for the child process proxy boolean auto-detected Whether to route through the proxy (false for tasks) apps object Overrides for workspace packages, keyed by relative path turbo boolean true Set false to use direct spawning instead of turborepo

Each apps entry has the same shape (name, script, appPort, proxy). When apps is present, top-level fields apply only in single-app mode.

package.json "portless" key

Instead of a separate portless.json, you can add a "portless" key to your package.json. A string value is shorthand for setting the name:

Copy & paste — that's it
{ "portless": "myapp" }

An object supports all per-app fields (name, script, appPort, proxy):

Copy & paste — that's it
{ "portless": { "name": "myapp", "script": "dev:app" } }

Precedence (closest wins): CLI flags > package.json "portless" key > portless.json app entry > defaults.