Skip to main content

portless

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.

Jump to install

Source facts

Repository
vercel-labs/portless
Last source activity
September 14, 2026 at 14:03
Detected SKILL.md language
English
Stars
12,609
Forks
425

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
portless
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.
# 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. ```bash # 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 ```bash # 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, TLDs) 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 ### 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: ```bash portless # -> runs "dev" script, https://<project>.localhost pnpm dev # -> works without portless, plain "next dev" ``` Use an optional `portless.json` to override defaults (name, script, port): ```json { "name": "myapp" } ``` ```bash 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): ```json { "apps": { "apps/web": { "name": "myapp" }, "apps/api": { "name": "api.myapp" } } } ``` ```bash 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: ```json { "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. When `portless` runs from a workspace root, it uses the existing Turbo integration to preserve task ordering when either `turbo.json` or `turbo.jsonc` is readable. Set `"turbo": false` in the root portless configuration to use direct spawning instead. ### package.json scripts You can still use portless directly in scripts: ```json { "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 ```bash 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: ```bash # 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: ```bash PORTLESS=0 pnpm dev # Bypasses proxy, uses default port ``` When a proxied command is stopped with Ctrl+C, portless waits for its process tree to exit. A second Ctrl+C forwards another interrupt, and remaining descendants are terminated after a short grace period. ## 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 Outside LAN mode, the proxy and its HTTP redirect listener bind only to the IPv4 and IPv6 loopback addresses, `127.0.0.1` and `::1`. They do not accept connections through LAN, VPN, or other network interfaces. `.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. When configured TLDs overlap (e.g. `example.com` and `dev.example.com`), hostnames are matched against the longest TLD first, regardless of configuration order. `PORTLESS_TLD` accepts the same comma separated list format, e.g. `PORTLESS_TLD=localhost,test`. TLDs can be multi-segment DNS names such as `dev.example.com`, so local URLs can mirror production structure (`myapp.dev.example.com`). Each label follows DNS rules: lowercase letters, digits, interior hyphens, 63 characters per label, 253 total. Strict OAuth providers that reject `.localhost` redirect URIs accept a real domain like `https://myapp.dev.example.com/api/auth/callback/google`. 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. Injection reaches through a package script whose command starts with the framework or a known runner (`"dev": "vite"`, `"dev": "bunx vite"`). Only the framework's server commands get the flags (`dev`, `serve`, `preview`, `start`, a bare `vite`, or `vite [root]`); a command that does not serve, such as `vite build`, `vite optimize`, `vp test` or `astro check`, rejects them and is left alone. Expo connection modes (`--localhost`, `--lan`, `--tunnel`) are preserved while the assigned port is still injected. A script portless cannot classify is left alone too: a flag before the subcommand on a CLI whose flag grammar it does not track (`vp --mode dev build`). Portless also leaves a script alone when appending flags to it would not work: a compound command (`&&`, `|`, `;`), a trailing `#` comment, its own `--` option terminator, an env prefix (`NODE_ENV=production vite`), delegation to another script (`"dev": "npm run dev:vite"`), or runner flags before the script name (`bun run --bun dev`). Those keep their own port, so set it in the script yourself. ### State directory Portless stores its state (routes, PID file, port file) in `~/.portless`. When the proxy runs under sudo, this remains the invoking user's home directory so unprivileged apps and the proxy share route registrations. 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, single or multi-segment (e.g. localhost,dev.example.com) | | `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). WebSockets work over both HTTP/1.1 (Upgrade) and HTTP/2 (RFC 8441 extended CONNECT), so dev server HMR works through the proxy. First run generates a local CA and adds it to the system trust store. After that, no prompts and no browser warnings. ```bash 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. On WSL, it updates both the Linux trust store and the Windows current-user Root store so Windows browsers trust portless HTTPS certificates. ### LAN mode ```bash portless proxy start --lan portless proxy start --lan --https portless proxy start --lan --ip 192.168.1.42 ``` `--lan` explicitly binds the proxy to the IPv4 and IPv6 unspecified addresses, `0.0.0.0` and `::`, and advertises `<name>.local` hostnames over mDNS so devices 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`: ```js // 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`: ```bash 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`: ```bash 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: ```bash 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
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub