Portless - Named .localhost URLs

SkillDev tools

Replace port numbers with stable named .localhost URLs for local development

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Portless - Named .localhost URLs skill

What this skill tells your AI

The instructions your AI receives, as published by jellydn/my-ai-tools in skills/portless-local/SKILL.md and read by ahel’s review.

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

Note: By default, use HTTP (http://myapp.localhost). Only enable HTTPS (--https or PORTLESS_HTTPS=1) if the user specifically requests it (e.g., for OAuth, secure cookies, or HTTPS-only features).

Why Portless?

Local dev with port numbers is fragile. Portless fixes that by giving each dev server a stable, named .localhost URL.

ProblemWith PortsWith Portless
Port conflictsTwo projects on :3000 = EADDRINUSEAuto-assigned ports, named URLs - no collisions
Memorizing ports"Was the API on 3001 or 8080?"Always http://api.localhost
Wrong app on refreshStop one server, start another on same port = confusionNamed URLs eliminate this
Monorepo chaosEvery service needs a unique portDistinct hostnames for each service
Agent confusionAI agents guess/hardcode wrong portshttp://myapp.localhost is deterministic
Cookie/storage clashesCookies bleed across ports on localhostEach .localhost subdomain gets its own scope
Hardcoded configCORS, OAuth, .env break when ports changeURLs are stable across restarts
Sharing URLs"What port is that on?" in SlackEveryone uses the same named URL
Browser historylocalhost:3000 history is a jumbleNamed URLs keep things organized

Installation

# Global (recommended)
npm install -g portless

# Or as a project dev dependency
npm install -D portless

Note: portless is pre-1.0. When installed per-project, different contributors may run different versions.

Usage

Invoke via skill command or use CLI directly:

# Via skill command
/portless-local <NAME> <COMMAND> [OPTIONS]

# Or use CLI directly
portless <NAME> <COMMAND> [OPTIONS]

Commands

Run an App

portless run [--name <name>] <cmd> [args...]   # Infers name from package.json, git root, or directory
portless <name> <cmd> [args...]                # Explicit name, no inference

portless run infers the project name from package.json, git root, or directory name. Use --name to override the inferred name while still applying worktree prefixes.

FlagDescription
--name <name>Override the inferred base name (worktree prefix still applies). Only for portless run.
--app-port <number>Use a fixed port for the app instead of auto-assignment. Also configurable via PORTLESS_APP_PORT.
--forceOverride an existing route registered by another process

Examples:

portless run next dev                    # Infer name from project
portless run --name myapp next dev       # Override inferred name
portless myapp next dev                  # Explicit name
portless api pnpm start                  # API service
portless docs.myapp next dev             # Subdomain

Get a Service URL

portless get <name>

Print the URL for a service. Useful for wiring services together in scripts or env vars:

BACKEND_URL=$(portless get backend)

Applies worktree prefix detection by default. Use --no-worktree to skip it.

Alias (Static Routes)

portless alias <name> <port>              # Register a static route
portless alias <name> <port> --force     # Force override existing
portless alias --remove <name>            # Remove the alias

Register a route for a service not managed by portless (e.g. a Docker container). Aliases persist across stale-route cleanup.

portless alias my-postgres 5432     # -> http://my-postgres.localhost
portless alias redis 6379           # -> http://redis.localhost
portless alias --remove my-postgres # Remove the alias

List Routes

portless list

Shows active routes and their assigned ports.

Trust the CA

portless trust

Adds the portless certificate authority to your system trust store. Required once for HTTPS with auto-generated certs.

If you skipped the trust prompt on first run, run portless trust to add the CA later.

HTTPS & HTTP/2

HTTP/2 + TLS is enabled by default for faster dev server page loads.

Why HTTP/2 matters: Browsers limit HTTP/1.1 to 6 connections per host, which bottlenecks dev servers serving many unbundled files. HTTP/2 multiplexes all requests over a single connection.

First run: Generates a local CA and server certs, then adds the CA to your system trust store. After that, no prompts, no browser warnings.

Custom certificates: Use your own certs (e.g., from mkcert):

portless proxy start --cert ./cert.pem --key ./key.pem

Disable HTTPS: Use --no-tls to run with plain HTTP on port 80:

portless proxy start --no-tls
portless myapp next dev --no-tls

Clean Up

portless clean

Stops the proxy, removes the CA from OS trust store, deletes allowlisted files under ~/.portless, the system state directory, and removes the portless block from /etc/hosts. May prompt for elevated privileges.

Proxy Control

Start Proxy
portless proxy start
FlagDescription
-p, --port <number>Proxy port (default: 443, or 80 with --no-tls). Auto-elevates with sudo.
--no-tlsDisable HTTPS (use plain HTTP on port 80)
--httpsEnable HTTPS (default, accepted for compatibility)
--lanEnable LAN mode (mDNS .local domains for real device testing)
--ip <address>Override auto-detected LAN IP (use with --lan)
--tld <tld>Use a custom TLD instead of .localhost (e.g. .test)
--cert <path>Custom TLS certificate
--key <path>Custom TLS private key
--foregroundRun in foreground instead of daemon mode
Stop Proxy
portless proxy stop

LAN Mode

Access services from phones and other devices on the same WiFi via mDNS (.local domains):

portless proxy start --lan
portless proxy start --lan --https
portless proxy start --lan --ip 192.168.1.42   # Manual IP override

Make it permanent by adding export PORTLESS_LAN=1 to your shell profile. Portless also remembers LAN mode via proxy.lan, so a stopped LAN proxy starts in LAN mode again.

Framework notes for LAN:

  • Next.js: Add allowedDevOrigins: ['myapp.local', '*.myapp.local'] to next.config.js
  • Vite / React Router / SvelteKit / Astro: Handled automatically via __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS
  • Expo / React Native: Add NSAllowsLocalNetworking to app.json for iOS ATS

Hosts

portless hosts sync     # Add current routes to /etc/hosts
portless hosts clean    # Remove portless entries from /etc/hosts

Auto-sync is on by default. Set PORTLESS_SYNC_HOSTS=0 to disable.

Bypass Portless

PORTLESS=0 pnpm dev

Runs the command directly without the proxy.

Info

portless --help
portless --version

Common Use Cases

1. Basic Development Server

# Next.js
portless myapp next dev
# -> http://myapp.localhost

# Vite (auto-detected, --port injected)
portless myapp vite dev
# -> http://myapp.localhost

# Express
portless api node server.js
# -> http://api.localhost

2. Multiple Services with Subdomains

# API service
portless api.myapp pnpm start
# -> http://api.myapp.localhost

# Documentation
portless docs.myapp next dev
# -> http://docs.myapp.localhost

# Admin dashboard
portless admin.myapp npm run dev
# -> http://admin.myapp.localhost

3. Use in package.json

{
	"scripts": {
		"dev": "portless myapp next dev",
		"dev:http": "portless myapp next dev --no-tls"
	}
}

4. Git Worktree Support

portless run auto-detects git worktrees. The branch name is prepended as a subdomain:

# Main worktree
portless run next dev
# -> http://myapp.localhost

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

Put portless run in your package.json once and it works everywhere - no collisions, no --force.

5. Custom TLD

# Use .test TLD instead of .localhost
portless proxy start --tld test
portless myapp next dev
# -> http://myapp.test

Recommended TLDs:

  • .localhost - Default, auto-resolves to 127.0.0.1 in most browsers
  • .test - IANA-reserved, no collision risk (recommended)
  • Avoid: .local (conflicts with mDNS/Bonjour), .dev (Google-owned, forces HTTPS via HSTS)

6. Static Aliases for External Services

# Docker container running Postgres
portless alias my-postgres 5432
# -> http://my-postgres.localhost

# Redis server
portless alias redis 6379
# -> http://redis.localhost

7. Wire Services Together

# Get backend URL for frontend env
BACKEND_URL=$(portless get backend)
echo "VITE_API_URL=$BACKEND_URL" > .env.local
portless frontend vite dev

How It Works

Browser (myapp.localhost) -> HTTP Proxy (port 80) -> App (random port 4000-4999)
  1. Portless runs an HTTP reverse proxy on port 80 (or HTTPS on 443 if enabled)
  2. Each app registers a route mapping hostname to assigned port
  3. Requests to http://<name>.localhost are proxied to the app
  4. Optional HTTPS: Auto-generates local CA and trusts it on first run
  5. Auto-elevates with sudo on macOS/Linux for port binding

Framework Support

Portless auto-detects and configures:

FrameworkSupportNotes
Next.js✅ NativeRespects PORT env var
Express✅ NativeRespects PORT env var
Nuxt✅ NativeRespects PORT env var
Vite✅ InjectedAuto-adds --port flag
Astro✅ InjectedAuto-adds --port flag
React Router✅ InjectedAuto-adds --port flag
Angular✅ InjectedAuto-adds --port and --host
Expo✅ InjectedAuto-adds --port and --host
React Native✅ InjectedAuto-adds --port and --host

Configuration

Portless is configured through environment variables. No config files needed.

Environment Variables

VariableDescriptionDefault
PORTLESS_PORTProxy port443 (HTTPS) / 80 (HTTP)
PORTLESS_HTTPSHTTPS on by default; set to 0 to disable (same as --no-tls)on
PORTLESS_LANSet to 1 to always enable LAN mode (mDNS .local domains)off
PORTLESS_TLDUse a custom TLD instead of .localhost (e.g. test)localhost
PORTLESS_APP_PORTUse a fixed port for the app (skip auto-assignment)random 4000-4999
PORTLESS_SYNC_HOSTSSet to 0 to disable auto-sync of /etc/hostson
PORTLESS_STATE_DIROverride the state directorysee below
PORTLESSSet to 0 to bypass the proxyenabled

State Directory

Portless stores state (routes, PID file, port file, TLS marker) in a directory that depends on the proxy port:

ConditionPath
Port below 1024 (sudo, macOS/Linux)/tmp/portless
Port 1024+ (no sudo)~/.portless
Windows (any port)~/.portless

Override with PORTLESS_STATE_DIR.

State Files

FilePurpose
routes.jsonMaps hostnames to ports
routes.lockPrevents concurrent writes
proxy.pidPID of the running proxy
proxy.portPort the proxy is listening on
proxy.logProxy daemon log output
proxy.lanRemembers LAN mode and stores the last known LAN IP

Port Assignment

Apps get a random port in the 4000-4999 range. Portless sets PORT and usually HOST before running your command. Most frameworks respect PORT automatically. For frameworks that ignore it (Vite, Astro, React Router, Angular, Expo, React Native), portless auto-injects the right --port flag and, when needed, a matching --host flag.

Troubleshooting

Port 443 permission denied

# Portless auto-elevates with sudo, but if it fails:
sudo portless proxy start

# Or use HTTP mode on a different port
portless myapp next dev --no-tls -p 8080

Certificate warning

Trust the local CA on first run. Run portless trust if needed.

Name collision

# Each worktree gets unique subdomain automatically
# Or use different names:
portless myapp-v2 next dev

Comparison

ToolTypeURLsUse Case
portlessLocal proxyhttp://myapp.localhostClean local dev URLs
ngrokPublic tunnelhttps://random.ngrok.ioShare with others
cloudflaredPublic tunnelhttps://myapp.trycloudflare.comShare with others

Related

Signals

GitHub stars
120
Forks
13
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
portless-local
Source
github.com/jellydn/my-ai-tools