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:
portless # runs "dev" script, https://<project>.localhostportless.json
Use an optional portless.json to override defaults:
{ "name": "myapp" }portless # runs "dev" script, https://myapp.localhostThe name is inferred from package.json if not set in config. The script defaults to "dev".
Fields
| Field | Type | Default | Description |
|---|---|---|---|
name | string | inferred | Base app name. Worktree prefix still applies. |
script | string | "dev" | Name of a package.json script to run. |
appPort | number | auto | Fixed port for the child process. |
proxy | boolean | auto | Whether to route through the proxy. Auto-detected from the command; set |
apps | object | Overrides for workspace packages, keyed by relative path. | |
turbo | boolean | true | Set false to use direct spawning instead of turborepo in multi-app mode. |
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:
{
"name": "@myorg/web",
"portless": "myapp"
}An object supports all per-app fields (name, script, appPort, proxy):
{
"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):
{
"apps": {
"apps/web": { "name": "myapp" },
"apps/api": { "name": "api.myapp" }
}
}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:
{
"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
| Variable | Description | Default |
|---|---|---|
PORTLESS_PORT | Proxy port | 443 (HTTPS) / 80 (HTTP) |
PORTLESS_HTTPS | HTTPS on by default; set to 0 to disable (same as --no-tls) | on |
PORTLESS_LAN | Set to 1 to always enable LAN mode (mDNS .local domains) | off |
PORTLESS_TLD | Use a custom TLD instead of .localhost (e.g. test) | localhost |
PORTLESS_APP_PORT | Use a fixed port for the app (skip auto-assignment) | random 4000--4999 |
PORTLESS_SYNC_HOSTS | Set to 0 to disable auto-sync of /etc/hosts | on |
PORTLESS_STATE_DIR | Override the state directory | ~/.portless |
PORTLESS | Set to 0 to bypass the proxy | enabled |
State directory
Portless stores state (routes, PID file, port file, TLS marker) in ~/.portless on all platforms. Override with PORTLESS_STATE_DIR.
State files
| File | Purpose |
|---|---|
routes.json | Maps hostnames to ports |
routes.lock | Prevents concurrent writes |
proxy.pid | PID of the running proxy |
proxy.port | Port the proxy is listening on |
proxy.log | Proxy daemon log output |
proxy.lan | Remembers 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.