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

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

## portless.json

Use an optional `portless.json` to override defaults:

```json
{ "name": "myapp" }
```

```bash
portless        # runs "dev" script, https://myapp.localhost
```

The name is inferred from `package.json` if not set in config. The script defaults to `"dev"`.

### Fields

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>Type</th>
      <th>Default</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>`name`</td>
      <td>string</td>
      <td>inferred</td>
      <td>Base app name. Worktree prefix still applies.</td>
    </tr>
    <tr>
      <td>`script`</td>
      <td>string</td>
      <td>`"dev"`</td>
      <td>Name of a `package.json` script to run.</td>
    </tr>
    <tr>
      <td>`appPort`</td>
      <td>number</td>
      <td>auto</td>
      <td>Fixed port for the child process.</td>
    </tr>
    <tr>
      <td>`proxy`</td>
      <td>boolean</td>
      <td>auto</td>
      <td>
        Whether to route through the proxy. Auto-detected from the command; set `false` for
        non-server scripts.
      </td>
    </tr>
    <tr>
      <td>`apps`</td>
      <td>object</td>
      <td></td>
      <td>Overrides for workspace packages, keyed by relative path.</td>
    </tr>
    <tr>
      <td>`turbo`</td>
      <td>boolean</td>
      <td>`true`</td>
      <td>Set `false` to use direct spawning instead of turborepo in multi-app mode.</td>
    </tr>
  </tbody>
</table>

Each `apps` entry has the same shape (`name`, `script`, `appPort`, `proxy`). When `apps` is present, top-level fields apply only in single-app mode.

### package.json "portless" key

Instead of a separate `portless.json`, you can add a `"portless"` key to your `package.json`. A string value is shorthand for setting the name:

```json
{
  "name": "@myorg/web",
  "portless": "myapp"
}
```

An object supports all per-app fields (`name`, `script`, `appPort`, `proxy`):

```json
{
  "name": "@myorg/web",
  "portless": { "name": "myapp", "script": "dev:app" }
}
```

The `package.json` `"portless"` key takes precedence over `portless.json` app entries but is overridden by CLI flags.

### Monorepo

One `portless.json` at the repo root covers all workspace packages. 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 needed for overrides. Packages not listed still auto-discover with names inferred from their `package.json`.

Without an `apps` map, hostnames follow the `<package>.<project>.localhost` convention. The project name comes from the most common npm scope across workspace packages (e.g. `@myorg/web` and `@myorg/api` produce project name `myorg`), falling back to the workspace root directory name. If a package's short name matches the project name, it gets the bare `<project>.localhost` without duplication.

### Turborepo

For turborepo projects, use `portless` as the `dev` script and the real command in a separate script:

```json
{
  "scripts": {
    "dev": "portless",
    "dev:app": "next dev"
  },
  "portless": { "name": "myapp", "script": "dev:app" }
}
```

`pnpm dev` at the root runs turbo, which runs `portless` in each package. Portless detects the package manager and runs `pnpm run dev:app` through the proxy. No `turbo.json` changes are needed.

People without portless installed can run `pnpm run dev:app` directly.

### Precedence

Closest config wins, like Prettier and ESLint:

For `name`: CLI `--name` flag > `package.json` `"portless"` key > `portless.json` app entry > `package.json` inference.

For `script`: CLI `--script` flag > `package.json` `"portless"` key > `portless.json` app entry > default `"dev"`.

For `appPort`: CLI `--app-port` flag > `PORTLESS_APP_PORT` env var > `package.json` `"portless"` key > `portless.json` app entry > auto-assigned.

## Environment variables

<table>
  <thead>
    <tr>
      <th>Variable</th>
      <th>Description</th>
      <th>Default</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>`PORTLESS_PORT`</td>
      <td>Proxy port</td>
      <td>`443` (HTTPS) / `80` (HTTP)</td>
    </tr>
    <tr>
      <td>`PORTLESS_HTTPS`</td>
      <td>HTTPS on by default; set to `0` to disable (same as `--no-tls`)</td>
      <td>on</td>
    </tr>
    <tr>
      <td>`PORTLESS_LAN`</td>
      <td>Set to `1` to always enable LAN mode (mDNS `.local` domains)</td>
      <td>off</td>
    </tr>
    <tr>
      <td>`PORTLESS_TLD`</td>
      <td>Use one or more TLDs, single or multi-segment (e.g. `localhost,dev.example.com`)</td>
      <td>`localhost`</td>
    </tr>
    <tr>
      <td>`PORTLESS_APP_PORT`</td>
      <td>Use a fixed port for the app (skip auto-assignment)</td>
      <td>random 4000 to 4999</td>
    </tr>
    <tr>
      <td>`PORTLESS_SYNC_HOSTS`</td>
      <td>Set to `0` to disable auto-sync of `/etc/hosts`</td>
      <td>on</td>
    </tr>
    <tr>
      <td>`PORTLESS_STATE_DIR`</td>
      <td>Override the state directory</td>
      <td>`~/.portless`</td>
    </tr>
    <tr>
      <td>`PORTLESS_TAILSCALE`</td>
      <td>Set to `1` to share apps on your Tailscale network</td>
      <td>off</td>
    </tr>
    <tr>
      <td>`PORTLESS_FUNNEL`</td>
      <td>Set to `1` to share apps publicly via Tailscale Funnel</td>
      <td>off</td>
    </tr>
    <tr>
      <td>`PORTLESS_NGROK`</td>
      <td>Set to `1` to share apps publicly via ngrok</td>
      <td>off</td>
    </tr>
    <tr>
      <td>`PORTLESS`</td>
      <td>Set to `0` to bypass the proxy</td>
      <td>enabled</td>
    </tr>
  </tbody>
</table>

## State directory

Portless stores state (routes, PID file, port file, TLS marker) in `~/.portless` on all platforms. Override with `PORTLESS_STATE_DIR`.

## State files

<table>
  <thead>
    <tr>
      <th>File</th>
      <th>Purpose</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>`routes.json`</td>
      <td>Maps hostnames to ports</td>
    </tr>
    <tr>
      <td>`routes.lock`</td>
      <td>Prevents concurrent writes</td>
    </tr>
    <tr>
      <td>`proxy.pid`</td>
      <td>PID of the running proxy</td>
    </tr>
    <tr>
      <td>`proxy.port`</td>
      <td>Port the proxy is listening on</td>
    </tr>
    <tr>
      <td>`proxy.log`</td>
      <td>Proxy daemon log output</td>
    </tr>
    <tr>
      <td>`proxy.lan`</td>
      <td>Remembers LAN mode and stores the last known LAN IP</td>
    </tr>
  </tbody>
</table>

## 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, VitePlus, Astro, React Router, Angular, Expo, React Native), portless auto-injects the right `--port` flag and, when needed, a matching `--host` 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`, or a bare `vite`); a command that does not serve, such as `vite build`, `vite optimize`, `vp test` or `astro check`, rejects them and is left alone. 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.
