---
title: "Getting Started"
description: "Replace port numbers with stable, named .localhost URLs. For humans and agents."
canonical_url: "https://portless.sh"
---
# Getting Started

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

```diff
- "dev": "next dev"                  # http://localhost:3000
+ "dev": "portless myapp next dev"   # https://myapp.localhost
```

## Install

**Global (recommended):**

```bash
npm install -g portless
```

**Or as a project dev dependency:**

```bash
npm install -D portless
```

> portless is pre-1.0. When installed per-project, different contributors may run different versions. The state directory format may change between releases, which can require re-running `portless trust`.

## Run your app

Just run `portless`. It reads the `"dev"` script from `package.json` and runs it through the proxy:

```bash
portless
# -> runs "dev" script, https://<project>.localhost
```

The app name is inferred from `package.json`, git root, or directory name. Use a `portless.json` to override (e.g. `{ "name": "myapp" }` for `https://myapp.localhost`).

You can also run with an explicit command:

```bash
portless myapp next dev
# -> https://myapp.localhost
```

HTTPS with HTTP/2 is enabled by default. On first run, portless generates a local CA, trusts it, and binds port 443 (auto-elevates with sudo on macOS/Linux). Use `--no-tls` for plain HTTP.

The proxy auto-starts when you run an app. A random port (4000--4999) is assigned via the `PORT` environment variable. Most frameworks (Next.js, Express, Nuxt, etc.) respect this automatically. For frameworks that ignore `PORT` (Vite, VitePlus, VitePress, Astro, React Router, Angular, Expo, React Native, Wrangler), portless auto-injects the right `--port` flag and, when needed, a matching `--host` flag (`--ip` for Wrangler). 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.

## Use in package.json

Your scripts stay clean:

```json
{
  "scripts": {
    "dev": "next dev"
  }
}
```

Then run `portless` or `portless run` to go through the proxy.

You can also use portless directly in scripts:

```json
{
  "scripts": {
    "dev": "portless myapp next dev"
  }
}
```

## Subdomains

Organize services with subdomains:

```bash
portless api.myapp pnpm start
# -> https://api.myapp.localhost

portless docs.myapp next dev
# -> https://docs.myapp.localhost
```

## Git Worktrees

`portless run` automatically detects git worktrees. In a linked worktree, the branch name is prepended as a subdomain so each worktree gets its own URL without any config changes:

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

Put `portless run` in `package.json` to apply these naming rules in each checkout.

Use `--name` to override the inferred base name while keeping the worktree prefix:

```bash
portless run --name myapp next dev   # -> https://fix-ui.myapp.localhost
```

Use `--prefix <label>` (or `PORTLESS_PREFIX`) to set the label yourself, with `""` for none. See [Configuration](/configuration) to change the label style or the whole hostname.

## Custom TLD

By default, portless uses `.localhost` which auto-resolves to `127.0.0.1` in most browsers. If you prefer a different TLD (e.g. `.test`), use `--tld`:

```bash
portless proxy start --tld test
portless myapp next dev
# -> https://myapp.test
```

The proxy auto-syncs `/etc/hosts` for route hostnames, so `.test` and other dev hostnames resolve on your machine. If a route hostname will not resolve, the command that registered it warns instead of failing silently and points you to `portless hosts sync`. Note that `.localhost` names resolve without a hosts entry on current macOS and Linux, so a skipped write is often invisible; a custom TLD needs the entry.

Repeat `--tld` to serve the same app names under multiple TLDs from one proxy:

```bash
portless proxy start --tld localhost --tld test
portless myapp next dev
# -> https://myapp.localhost
# -> https://myapp.test
```

When multiple TLDs are configured, `PORTLESS_URL` uses the first TLD. `PORTLESS_TLD` accepts the same comma separated list format, e.g. `PORTLESS_TLD=localhost,test`.

The `--tld` value accepts a lowercase DNS name such as `dev.example.com`, so a domain you own can be used as the "TLD":

```bash
portless proxy start --tld dev.example.com
portless myapp next dev
# -> https://myapp.dev.example.com
```

This gives local URLs the same structure as production, which keeps OAuth redirect URIs, cross-subdomain cookies, and host-based routing working the same way in both environments. Strict OAuth providers such as Google and Apple reject `.localhost` and `.test` redirect URIs but accept a real domain, so `https://myapp.dev.example.com/api/auth/callback/google` works as a redirect URI. Each label must follow DNS rules: lowercase letters, digits, and interior hyphens, with at most 63 characters per label and 253 characters total. The full hostname (`app.TLD`) is also subject to the 253-character limit.

A custom TLD is reachable only from the machine running the proxy, because outside LAN mode the proxy binds only to loopback. LAN mode serves apps under `.local` and ignores a custom `--tld`, so the two can't be combined.

Recommended: `.test` (IANA-reserved, no collision risk). Avoid `.local` (conflicts with mDNS/Bonjour) and `.dev` (Google-owned, forces HTTPS via HSTS).

## How it works

Portless runs an HTTPS reverse proxy on port 443 by default. Each app registers a route mapping its hostname to an assigned port. Requests to `https://<name>.localhost` are proxied to the app.

The proxy listens only on the IPv4 and IPv6 loopback addresses, `127.0.0.1` and `::1`, by default. Network interfaces are exposed only when LAN mode is explicitly enabled.

```
Browser (myapp.localhost) -> proxy (port 443) -> App (random port)
```

When portless itself returns an error (404 for an unknown host, 421 for a misdirected request, 502 when the app is unreachable, 508 for a request loop), browsers get an HTML page. Any client whose `Accept` header does not include `text/html`, such as curl or `fetch`, gets a small JSON body instead, for example `{"error":"bad_gateway","upstream":"myapp.localhost","message":"..."}`. Responses from your own app are forwarded unchanged. Hostnames outside the portless TLDs, such as a site a DNS blocklist points at 127.0.0.1, get 421 Misdirected Request instead of the app list.

## Proxy between portless apps

If your frontend dev server proxies API requests to another portless app, make the dev server proxy rewrite the `Host` header. Without it, portless routes the request back to the frontend in a loop, and responds with `508 Loop Detected` and a message pointing to this fix.

**Vite** (`vite.config.ts`):

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

**webpack-dev-server** (`webpack.config.js`):

```js
devServer: {
  proxy: [{
    context: ["/api"],
    target: "https://api.myapp.localhost",
    changeOrigin: true,
  }],
}
```

## Requirements

- Node.js 24+
- macOS, Linux, or Windows
