All skills

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

7,475 237 Updated 17 hours ago First seen 2 months agoactive
npx -y skilld add gh:vercel-labs/portless -s portless

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

Installation

Install globally (recommended) or as a project dev dependency. Do NOT use npx or pnpm dlx for one-off execution.

# Global (available everywhere)
npm install -g portless

# Or per-project dev dependency
npm install -D portless

When installed per-project, invoke via package.json scripts or npx portless (since the package is local, npx will not download anything).

Quick Start

# Install globally (or add -D to a project)
npm install -g portless

# Run your app (auto-starts the HTTPS proxy on port 443)
portless run next dev
# -> https://<project>.localhost

# Or with an explicit name
portless myapp next dev
# -> https://myapp.localhost

The proxy auto-starts when you run an app. You can also start it explicitly with portless proxy start. Auto-start reuses the configuration (port, TLS, TLD) from the most recent proxy run, so a restart or reboot does not silently revert to defaults. Explicit env vars always take priority.

In non-interactive environments (no TTY, or CI=1), portless exits with a descriptive error instead of prompting. Task runners like turborepo should pre-start the proxy.

Integration Patterns

package.json scripts

{
  "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

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:

# 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:

PORTLESS=0 pnpm dev   # Bypasses proxy, uses default port

How It Works

  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.
  2. portless <name> <cmd> assigns a random free port (4000-4999) via the PORT env var and registers the app with the proxy
  3. 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.

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 a directory that depends on the proxy port:

  • Port < 1024 (sudo required): /tmp/portless (macOS/Linux only)
  • Port >= 1024 (no sudo): ~/.portless
  • Windows: Always ~/.portless (no privileged port concept)

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_TLD Use a custom TLD instead of localhost (e.g. 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_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.

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

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, TLD, 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:

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

CLI Reference

Command Description
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 trust Add local CA to system trust store (for HTTPS)
portless clean Remove state, CA trust entry, and /etc/hosts block
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 --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 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> --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, 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.

Troubleshooting

Proxy not running

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:

portless proxy start

Port already in use

Another process is bound to the proxy port. Either stop it first, or use a different port:

portless proxy start -p 8080

Framework not respecting PORT

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.

For other frameworks that don't read PORT, pass the port manually:

  • Webpack Dev Server: use --port $PORT
  • Custom servers: read process.env.PORT and listen on it

Permission errors

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.

portless proxy start --https           # Auto-elevates with sudo for port 443
portless proxy start -p 1355 --https   # No sudo needed (URLs include :1355)
portless proxy stop                    # Stop (use sudo if started with sudo)

Safari can't find .localhost URLs

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.

Fix:

portless hosts sync    # Adds current routes to /etc/hosts
portless hosts clean   # Remove entries later

Auto-syncs /etc/hosts for route hostnames by default. Set PORTLESS_SYNC_HOSTS=0 to disable.

Browser shows certificate warning with --https

The local CA may not be trusted yet. Run:

portless trust

This adds the portless local CA to your system trust store. After that, restart the browser.

Remove portless from the machine

portless clean

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.

Proxy loop (508 Loop Detected)

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.

Fix: set changeOrigin: true in the proxy config (Vite, webpack-dev-server, etc.):

// vite.config.ts
proxy: {
  "/api": {
    target: "https://api.myapp.localhost",
    changeOrigin: true,
    ws: true,
  },
}

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.

Requirements

  • Node.js 20+
  • macOS, Linux, or Windows
  • 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)

Source: SKILL.md on GitHub

Indexed from github.com/vercel-labs/portless on branch main.

No curators have added this skill yet. Be the first to include it in a collection.