Portless is a routing layer. It pairs with a process supervisor that owns lifecycle. Three common combos:
The whole stack speaks YAML and gives you health checks, restart policies, dependencies, and an MCP server.
# process-compose.yaml — supervisor owns processes
processes:
myapp:
command: "uv run python -m myapp"
working_dir: "X:/path/to/myapp"
readiness_probe:
http_get: { host: localhost, port: 8000, path: / }
availability: { restart: always }
# Portless owns routing — aliases derive from supervisor config
portless proxy start --tld test
portless alias myapp 8000 # https://myapp.test → :8000
Single source of truth: process-compose.yaml. Aliases derive from it. See the process-compose-ops skill for the supervisor side.
When some services run in containers (databases, n8n, custom containers) and others run locally.
# Container started independently, listening on host port 5678
docker run -d -p 5678:5678 --name n8n n8nio/n8n
# Make it reachable at a named URL
portless alias n8n 5678
# → https://n8n.test
This decouples container lifecycle from portless. docker stop n8n doesn't affect portless's alias (URL just stops resolving until container's back up).
Same shape as Pattern A:
// ecosystem.config.js
module.exports = {
apps: [
{ name: 'myapp', script: 'python', args: '-m myapp', cwd: 'X:/path/myapp' }
]
};
# PM2 owns processes
pm2 start ecosystem.config.js
# Portless owns routing
portless alias myapp 8000
Note: PM2 5.x has 15+ known CVEs in its transitive npm dependencies (axios, lodash, tar, minimist...). Process Compose's Go-binary attack model is much narrower. New stacks should pick Pattern A.
For zero-config / one-off / monorepo cases, portless can spawn the process itself:
# Run a Next.js dev server through the proxy
portless myapp next dev
# → https://myapp.test, with auto-assigned port
# From a monorepo root, run all packages' dev scripts
portless
Limitations:
Good for: short-lived dev sessions, monorepos where everything is JS/TS and Vercel-like ergonomics matter.
Bad for: long-running services, dependency chains, anything you want supervised through a reboot. Use Pattern A instead.
Share local dev with teammates without a public deployment:
# Start the proxy
portless proxy start --tld test
# Run with --tailscale to register a tailnet URL too
portless myapp --tailscale next dev
# → https://myapp.test (you, local)
# → https://yourdevbox.your-team.ts.net (teammates, tailnet)
Requirements:
tailscale CLI installed and connected--funnel instead of --tailscale)See upstream docs (references/upstream-portless.md, section "Tailscale sharing") for the full setup.
portless myapp next dev # → https://myapp.test
portless api.myapp pnpm start # → https://api.myapp.test
portless docs.myapp next dev # → https://docs.myapp.test
Add --wildcard so any unregistered subdomain falls back to the parent:
portless proxy start --wildcard --tld test
# Now tenant1.myapp.test → routes to myapp (whatever's registered)
Useful for multi-tenant apps where you want to test tenant resolution locally.
Portless auto-detects git worktrees and prepends the branch name as a subdomain:
# Main worktree
cd X:/Forge/myapp
portless run next dev
# → https://myapp.test
# Linked worktree on branch "fix-ui"
cd X:/Forge/myapp/.worktrees/fix-ui
portless run next dev
# → https://fix-ui.myapp.test
No config — just works. Each worktree gets its own URL automatically, avoiding browser cookie/storage cross-contamination between branches.
BAD: use portless's spawn mode for production-equivalent local services
GOOD: use Pattern A (Process Compose supervisor + portless routing)
BAD: let two different stacks fight over the same TLD
GOOD: pick TLD per machine, document it; or use different ports
BAD: hardcode portless URLs in service config (e.g. CORS allowlists)
GOOD: read PORTLESS_URL env var that portless injects into spawned processes
(Pattern D only); or use SERVICE_URL env injection in your supervisor
BAD: install portless globally without pinning a version
GOOD: pin version: npm install -g portless@0.13.0; record in your repo
process-compose-ops skill for the supervisor side of Pattern Areferences/upstream-portless.md for full CLI reference (auto-port assignment, etc.)references/tld-selection.md for picking the right TLD up front