设置并使用 portless,以命名的本地开发服务器 URL(例如 https://myapp.localhost)替代 http://localhost:3000。在将 portless 集成到项目中、配置开发服务器名称、设置本地代理、使用 .localhost 域名或排查端口/代理问题时使用。
Portless
用稳定的、命名的 .localhost URL 替换端口号。适用于人类和 AI 代理。
为什么选择 portless
- 端口冲突:两个项目默认使用相同端口时出现
EADDRINUSE - 记忆端口号:哪个应用在 3001 还是 8080?
- 刷新显示错误的应用:停止一个服务器,启动另一个同端口服务器,旧标签页显示错误内容
- Monorepo 放大效应:每个问题随仓库中每个服务而放大
- AI 代理测试错误端口:AI 代理猜测或硬编码错误端口
- Cookie/存储冲突:
localhost上的 Cookie 在应用间泄露;端口变化时 localStorage 丢失 - 配置中硬编码端口:CORS 白名单、OAuth 重定向、
.env文件在端口变化时失效 - 与队友共享 URL:“那个端口是多少?”成为 Slack 问题
- 浏览器历史无用:
localhost:3000的历史是无关项目的混合
安装
全局安装(推荐)或作为项目开发依赖安装。不要使用 npx 或 pnpm dlx 一次性执行。
# 全局安装(随处可用)
npm install -g portless
# 或作为项目开发依赖
npm install -D portless
当作为项目依赖安装时,通过 package.json 脚本或 npx portless 调用(由于包在本地,npx 不会下载任何内容)。
快速开始
# 全局安装(或添加到项目 -D)
npm install -g portless
# 运行你的应用(自动启动 HTTPS 代理在 443 端口)
portless run next dev
# -> https://<project>.localhost
# 或使用显式名称
portless myapp next dev
# -> https://myapp.localhost
当你运行应用时,代理会自动启动。你也可以用 portless proxy start 显式启动它。自动启动会复用最近一次代理运行的配置(端口、TLS、TLD),因此重启或重启动不会静默恢复默认值。显式环境变量始终优先。
在非交互式环境(无 TTY,或 CI=1)中,portless 会退出并显示描述性错误,而不是提示。像 turborepo 这样的任务运行器应预先启动代理。
集成模式
零配置(推荐)
裸 portless 开箱即用。它通过代理运行 package.json 中的 "dev" 脚本,从包名、git 根目录或目录推断应用名称:
portless # -> 运行 "dev" 脚本,https://<project>.localhost
pnpm dev # -> 不使用 portless 也能工作,纯 "next dev"
使用可选的 portless.json 覆盖默认值(名称、脚本、端口):
{ "name": "myapp" }
portless # -> 运行 "dev" 脚本,https://myapp.localhost
Monorepo
在仓库根目录放置一个 portless.json。Portless 从 pnpm-workspace.yaml 或 package.json 中的 "workspaces" 字段(npm、yarn、bun)发现包:
{
"apps": {
"apps/web": { "name": "myapp" },
"apps/api": { "name": "api.myapp" }
}
}
portless # 从仓库根目录:启动所有包含 "dev" 脚本的包
cd apps/web && portless # 只启动一个包
portless --script start # 运行 "start" 而不是 "dev"
apps 映射是可选的,仅提供名称覆盖。未列出的包会自动发现并推断名称。
没有 apps 映射时,主机名遵循 <package>.<project>.localhost。项目名称来自最常见的 npm 范围(例如 @myorg/web 和 @myorg/api 产生 myorg),回退到工作区根目录名称。如果包的短名称与项目名称匹配,则使用裸 <project>.localhost。
Turborepo
对于 turborepo 项目,将 portless 用作 dev 脚本,并将实际命令放在单独脚本中:
{
"scripts": { "dev": "portless", "dev:app": "next dev" },
"portless": { "name": "myapp", "script": "dev:app" }
}
pnpm dev 运行 turbo,turbo 在每个包中运行 portless。Portless 检测包管理器并运行 pnpm run dev:app 通过代理。
package.json 脚本
你仍然可以直接在脚本中使用 portless:
{
"scripts": {
"dev": "portless run next dev"
}
}
当你运行应用时,代理会自动启动。或者显式启动:portless proxy start。
多应用设置与子域名
portless myapp next dev # https://myapp.localhost
portless api.myapp pnpm start # https://api.myapp.localhost
portless docs.myapp next dev # https://docs.myapp.localhost
默认情况下,只有显式注册的子域名会被路由(严格模式)。使用 --wildcard 启动代理,允许已注册路由的任何子域名回退到该应用(例如 tenant1.myapp.localhost 路由到 myapp 应用)。精确匹配始终优先于通配符。
Git worktrees
portless run 自动检测 git worktrees。在链接的 worktree 中,分支名称作为子域名前缀添加,因此每个 worktree 获得唯一的 URL:
# 主 worktree(无前缀)
portless run next dev # -> https://myapp.localhost
# 分支 "fix-ui" 上的链接 worktree
portless run next dev # -> https://fix-ui.myapp.localhost
无需更改配置。将 portless run 放入 package.json 一次,即可在所有 worktrees 中工作。
绕过 portless
设置 PORTLESS=0 直接运行命令而不经过代理:
PORTLESS=0 pnpm dev # 绕过代理,使用默认端口
工作原理
portless proxy start在 443 端口启动一个 HTTPS 反向代理作为后台守护进程。在 macOS/Linux 上自动使用 sudo 提权;如果 sudo 不可用,则回退到 1355 端口。使用--no-tls在 80 端口上使用纯 HTTP。可通过-p/--port或PORTLESS_PORT环境变量配置。当你运行应用时,代理也会自动启动。portless <name> <cmd>通过PORT环境变量分配一个随机空闲端口(4000-4999),并在代理中注册该应用。- 浏览器访问
https://<name>.localhost;代理转发到应用的分配端口。
在 LAN 模式之外,代理及其 HTTP 重定向监听器仅绑定到 IPv4 和 IPv6 回环地址 127.0.0.1 和 ::1。它们不接受来自 LAN、VPN 或其他网络接口的连接。
.localhost 域名在 Chrome、Firefox 和 Edge 中原生解析到 127.0.0.1。Safari 依赖系统 DNS 解析器,在某些配置下可能无法处理 .localhost 子域名。如果需要,运行 portless hosts sync 将条目添加到 /etc/hosts。
使用 portless proxy start --tld localhost --tld test 从一个代理提供多个 TLD 下的相同应用名称。PORTLESS_URL 使用第一个配置的 TLD。PORTLESS_TLD 接受相同的逗号分隔列表格式,例如 PORTLESS_TLD=localhost,test。
大多数框架(Next.js、Express、Nuxt 等)会自动尊重 PORT 环境变量。对于忽略 PORT 的框架(Vite、VitePlus、Astro、React Router、Angular、Expo、React Native),portless 会自动注入正确的 --port 标志,并在需要时注入匹配的 --host CLI 标志。
状态目录
Portless 将其状态(路由、PID 文件、端口文件)存储在 ~/.portless 中。当代理在 sudo 下运行时,这仍然是调用用户的主目录,因此非特权应用和代理共享路由注册。使用 PORTLESS_STATE_DIR 环境变量覆盖。
环境变量
| 变量 | 描述 |
|---|---|
PORTLESS_PORT |
覆盖默认代理端口(默认:HTTPS 为 443,无 HTTPS 为 80) |
PORTLESS_APP_PORT |
为应用使用固定端口(跳过自动分配) |
PORTLESS_HTTPS |
默认启用 HTTPS;设置为 0 禁用(等同于 --no-tls) |
PORTLESS_LAN |
设置为 1 始终启用 LAN 模式(自动检测 LAN IP) |
PORTLESS_LAN_IP |
为 LAN 模式指定特定 LAN IP |
PORTLESS_TLD |
使用一个或多个 TLD(例如 localhost,test) |
PORTLESS_WILDCARD |
设置为 1 允许未注册的子域名回退到父路由 |
PORTLESS_SYNC_HOSTS |
设置为 0 禁用 /etc/hosts 的自动同步(默认启用) |
PORTLESS_TAILSCALE |
设置为 1 在 Tailscale 网络上共享应用(等同于 --tailscale) |
PORTLESS_FUNNEL |
设置为 1 通过 Tailscale Funnel 公开共享应用(等同于 --funnel) |
PORTLESS_NGROK |
设置为 1 通过 ngrok 公开共享应用(等同于 --ngrok) |
PORTLESS_STATE_DIR |
覆盖状态目录 |
PORTLESS=0 |
绕过代理,直接运行命令 |
HTTP/2 + HTTPS
默认启用 HTTPS 和 HTTP/2(对于包含许多文件的开发服务器,页面加载更快)。WebSocket 通过 HTTP/1.1(Upgrade)和 HTTP/2(RFC 8441 扩展 CONNECT)均能工作,因此开发服务器 HMR 可以通过代理工作。首次运行会生成本地 CA 并将其添加到系统信任存储。之后,没有提示,也没有浏览器警告。
portless proxy start --cert ./c.pem --key ./k.pem # 使用自定义证书
portless proxy start --no-tls # 禁用 HTTPS(纯 HTTP)
portless trust # 稍后添加 CA 到信任存储
在 Linux 上,portless trust 支持 Debian/Ubuntu、Arch、Fedora/RHEL/CentOS 和 openSUSE(通过 update-ca-certificates 或 update-ca-trust)。在 Windows 上,它使用 certutil 将 CA 添加到系统信任存储。在 WSL 上,它会同时更新 Linux 信任存储和 Windows 当前用户根存储,以便 Windows 浏览器信任 portless HTTPS 证书。
LAN 模式
portless proxy start --lan
portless proxy start --lan --https
portless proxy start --lan --ip 192.168.1.42
--lan 显式将代理绑定到 IPv4 和 IPv6 通配地址 0.0.0.0 和 ::,并通过 mDNS 广播 <name>.local 主机名,以便同一 Wi-Fi 上的设备可以访问你的应用。Portless 自动检测你的 LAN IP 并自动跟随网络变化,但你可以使用 --ip <address> 或 PORTLESS_LAN_IP 环境变量指定特定地址。设置 PORTLESS_LAN=1 使每次代理启动时默认进入 LAN 模式。
Portless 通过 proxy.lan 记住 LAN 模式,因此如果你停止 LAN 代理并再次启动,它仍处于 LAN 模式。所有代理设置(端口、TLS、TLD、LAN)都会持久化并在自动启动时复用,除非被显式标志或环境变量覆盖。使用 PORTLESS_LAN=0 进行一次启动以切换回 .localhost 模式。如果代理已运行且具有不同的显式 LAN/TLS/TLD 设置,portless 会警告并要求你先停止它。
LAN 模式依赖于 portless 启动的系统 mDNS 助手:macOS 包含 dns-sd,而 Linux 使用 avahi-publish-address(来自 avahi-utils,通过 sudo apt install avahi-utils 或你的发行版工具安装)。
-
Next.js:将你的
.local主机名添加到allowedDevOrigins:// next.config.js module.exports = { allowedDevOrigins: ["myapp.local", "*.myapp.local"], }; -
Expo / React Native:portless 始终注入
--port。React Native 还会获得--host 127.0.0.1。Expo 在非 LAN 模式下获得--host localhost,但在 LAN 模式下,portless 让 Metro 保持其默认的 LAN 主机行为,而不是强制--host或HOST。
Tailscale 共享
使用 --tailscale 与 Tailscale 网络上的队友共享开发服务器,或使用 --funnel 暴露到公共互联网:
portless myapp --tailscale next dev
# -> https://myapp.localhost (本地)
# -> https://devbox.yourteam.ts.net (tailnet)
portless myapp --funnel next dev
# -> https://myapp.localhost (本地)
# -> https://devbox.yourteam.ts.net (公共互联网)
在 --tailscale 或 --funnel 注册 HTTPS URL 之前,必须启用 Tailscale HTTPS 证书。Funnel 还必须为 tailnet 和节点启用,然后 --funnel 才能注册公共 URL。如果缺少任一设置,portless 会在启动子进程之前退出。
每个 --tailscale 应用都根挂载在其自己的 Tailscale HTTPS 端口(443,然后是 8443、8444 等)上,因此无需框架 basePath 配置。设置 PORTLESS_TAILSCALE=1 默认共享每个应用。portless list 显示本地和 tailnet URL。应用退出时,Tailscale serve 注册会被清理。需要安装并连接 tailscale CLI,并启用 Tailscale HTTPS 证书。
ngrok 共享
使用 --ngrok 通过 ngrok 将开发服务器暴露到公共互联网:
portless myapp --ngrok next dev
# -> https://myapp.localhost (本地)
# -> https://abc123.ngrok.app (公共互联网)
设置 PORTLESS_NGROK=1 在 portless 运行应用时默认启用 ngrok。portless list 显示本地和 ngrok URL。应用退出时,ngrok 隧道会被清理。需要安装 ngrok CLI 并使用 ngrok config add-authtoken <token> 进行身份验证。
操作系统启动服务
当用户希望代理在重启后自动启动时,使用 service 命令:
portless service install
portless service install --lan
portless service install --wildcard
PORTLESS_STATE_DIR=~/.portless-lan PORTLESS_LAN=1 portless service install
portless service status
portless service uninstall
该服务使用 portless 默认值,除非提供了安装选项或 PORTLESS_* 环境变量:HTTPS 在 443 端口上使用 .localhost 名称。service install 接受代理选项,包括 --port、--no-tls、--lan、--ip、--tld、--wildcard、--cert 和 --key。使用 --state-dir <path> 或 PORTLESS_STATE_DIR=<path> 选择服务状态和日志写入位置。
所选的服务配置会写入 launchd、systemd 或 Task Scheduler,并在重启后复用。portless service status 报告安装的端口、HTTPS 模式、TLD、LAN 模式、通配符模式和状态目录。macOS 和 Linux 安装一个 root 拥有的服务,以便端口 443 可以在启动时绑定。Windows 安装一个作为 SYSTEM 运行的 Task Scheduler 启动任务。安装和卸载可能需要管理员权限。portless clean 会自动移除该服务。
CLI 参考
| 命令 | 描述 |
|---|---|
portless |
通过代理运行 dev 脚本 |
portless |
从 monorepo 根目录:运行所有工作区包 |
portless --script <name> |
运行特定的 package.json 脚本(默认:dev) |
portless run [cmd] [args...] |
从项目推断名称,通过代理运行(自动启动) |
portless run --name <name> <cmd> |
覆盖推断的基础名称(worktree 前缀仍然适用) |
portless <name> <cmd> [args...] |
在 https://<name>.localhost 运行应用(自动启动代理) |
portless get <name> |
打印服务的 URL(用于跨服务连接) |
portless get <name> --no-worktree |
打印不带 worktree 前缀的 URL |
portless list |
显示活动路由 |
portless doctor |
检查代理、路由、DNS、CA 信任和 LAN 先决条件 |
portless trust |
将本地 CA 添加到系统信任存储(用于 HTTPS) |
portless clean |
移除状态、CA 信任条目和 /etc/hosts 块 |
portless prune |
终止崩溃会话中的孤立开发服务器 |
portless prune --force |
使用 SIGKILL 而不是 SIGTERM 终止孤立进程 |
portless proxy start |
启动 HTTPS 代理作为守护进程(端口 443,自动提权) |
portless proxy start --no-tls |
无 HTTPS 启动(纯 HTTP 在端口 80) |
portless proxy start --lan |
以 LAN 模式启动(mDNS .local,自动跟随 LAN IP 变化) |
portless proxy start -p <number> |
在自定义端口上启动代理 |
portless proxy start --tld test |
使用 .test 代替 .localhost |
portless proxy start --tld localhost --tld test |
从一个代理提供两个 TLD |
portless proxy start --foreground |
在前台启动代理(用于调试) |
portless proxy start --wildcard |
允许未注册的子域名回退到父路由 |
portless proxy stop |
停止代理 |
portless service install |
在操作系统启动时启动 HTTPS 代理 |
portless service install --lan |
以 LAN 模式启动服务 |
portless service install --wildcard |
在启动服务中持久化通配符路由 |
portless service status |
显示服务和代理状态 |
portless service uninstall |
移除启动服务 |
portless alias <name> <port> |
注册静态路由(例如用于 Docker 容器) |
portless alias <name> <port> --force |
覆盖现有路由 |
portless alias --remove <name> |
移除静态路由 |
portless hosts sync |
将路由添加到 /etc/hosts(修复 Safari) |
portless hosts clean |
从 /etc/hosts 移除 portless 条目 |
portless <name> --app-port <n> <cmd> |
为应用使用固定端口而不是自动分配 |
portless <name> --tailscale <cmd> |
在 Tailscale 网络上共享应用(tailnet) |
portless <name> --funnel <cmd> |
通过 Tailscale Funnel 公开共享应用 |
portless <name> --ngrok <cmd> |
通过 ngrok 公开共享应用 |
portless <name> --force <cmd> |
终止现有进程并接管其路由 |
portless --name <name> <cmd> |
强制 <name> 作为应用名称(绕过子命令分发) |
portless <name> -- <cmd> [args...] |
停止标志解析;-- 之后的所有内容传递给子进程 |
portless --help / -h |
显示帮助 |
portless run --help |
显示子命令的帮助(也适用于:alias、hosts、clean) |
portless --version / -v |
显示版本 |
保留名称: run、get、alias、hosts、list、doctor、trust、clean、prune、proxy 和 service 是子命令,不能直接用作应用名称。使用 portless run <cmd> 推断名称,或使用 portless --name <name> <cmd> 强制任何名称,包括保留名称。
portless.json
可选的配置文件。Portless 在当前目录中查找它。
| 字段 | 类型 | 默认值 | 描述 |
|---|---|---|---|
name |
string | 从 package.json 推断 | 基础应用名称(worktree 前缀仍然适用) |
script |
string | "dev" |
要运行的 package.json 脚本名称 |
appPort |
number | 自动分配 | 子进程的固定端口 |
proxy |
boolean | 自动检测 | 是否通过代理路由(false 用于任务) |
apps |
object | 工作区包的覆盖,键为相对路径 | |
turbo |
boolean | true |
设置为 false 使用直接生成而不是 turborepo |
每个 apps 条目具有相同结构(name、script、appPort、proxy)。当存在 apps 时,顶级字段仅适用于单应用模式。
package.json 中的 "portless" 键
除了单独的 portless.json,你还可以在 package.json 中添加 "portless" 键。字符串值是设置名称的简写:
{ "portless": "myapp" }
对象支持所有每个应用的字段(name、script、appPort、proxy):
{ "portless": { "name": "myapp", "script": "dev:app" } }
优先级(最近的获胜):CLI 标志 > package.json 中的 "portless" 键 > portless.json 应用条目 > 默认值。
故障排除
运行诊断
当本地路由或 HTTPS 行为异常时,首先使用 portless doctor。它是只读的,检查 Node.js、状态目录权限、代理存活、路由条目、主机名解析、本地 CA 信任和 LAN 模式先决条件。
代理未运行
当你使用 portless <name> <cmd> 运行应用时,代理会自动启动。如果它没有启动(例如端口冲突),请手动启动:
portless proxy start
端口已被占用
另一个进程已绑定到代理端口。要么先停止它,要么使用不同的端口:
portless proxy start -p 8080
框架不尊重 PORT
对于忽略 PORT 环境变量的框架,Portless 会自动注入正确的 --port 标志,并在需要时注入匹配的 --host 标志:Vite、VitePlus (vp)、Astro、React Router、Angular、Expo 和 React Native。SvelteKit 内部使用 Vite,会自动处理。
对于其他不读取 PORT 的框架,手动传递端口:
- Webpack Dev Server:使用
--port $PORT - 自定义服务器:读取
process.env.PORT并监听它
权限错误
默认端口(HTTP 为 80,HTTPS 为 443)在 macOS 和 Linux 上需要 sudo。Portless 在需要时自动使用 sudo 提权。如果 sudo 不可用,则回退到端口 1355(无需 sudo)。在 Windows 上,无需提权。
portless proxy start --https # 自动使用 sudo 提权以使用端口 443
portless proxy start -p 1355 --https # 无需 sudo(URL 包含 :1355)
portless proxy stop # 停止(如果使用 sudo 启动,则使用 sudo)
Safari 无法找到 .localhost URL
Safari 依赖系统 DNS 解析器处理 .localhost 子域名,在某些 macOS 配置上可能无法解析。Chrome、Firefox 和 Edge 有内置处理。
修复:
portless hosts sync # 将当前路由添加到 /etc/hosts
portless hosts clean # 稍后移除条目
默认情况下,自动同步 /etc/hosts 以包含路由主机名。设置 PORTLESS_SYNC_HOSTS=0 禁用。
浏览器显示证书警告(使用 --https)
本地 CA 可能尚未被信任。运行:
portless trust
这会将 portless 本地 CA 添加到系统信任存储。之后,重启浏览器。
从机器中移除 portless
portless clean
如果需要,停止代理,从信任存储中移除 portless CA(当 portless 添加它时),删除状态目录下的已知文件,并移除 portless 的 /etc/hosts 块。在 macOS/Linux 上可能需要 sudo。如果信任存储移除失败,portless 会保留其 CA 证书和密钥,以便稍后的 portless clean 可以安全重试。
代理循环(508 循环检测)
如果你的开发服务器将请求代理到另一个 portless 应用(例如 Vite 将 /api 代理到 api.myapp.localhost),代理必须重写 Host 头。否则,portless 会将请求路由回原始应用,造成无限循环。
修复:在代理配置中设置 changeOrigin: true(Vite、webpack-dev-server 等):
// vite.config.ts
proxy: {
"/api": {
target: "https://api.myapp.localhost",
changeOrigin: true,
ws: true,
},
}
Portless 自动在子进程中设置 NODE_EXTRA_CA_CERTS,以便 Node.js 信任 portless CA。如果你在 portless 之外运行单独的 Node.js 进程,请手动指向 CA:NODE_EXTRA_CA_CERTS=~/.portless/ca.pem。或者,使用 --no-tls 进行纯 HTTP。
Tailscale 不工作
如果 --tailscale 或 --funnel 失败:
tailscale status # 检查是否已连接
tailscale up # 连接到你的 tailnet
需要安装 Tailscale CLI(https://tailscale.com/download)并在 PATH 中。
ngrok 不工作
如果 --ngrok 失败:
ngrok version # 检查是否已安装
ngrok config add-authtoken <token> # 配置身份验证
需要安装 ngrok CLI(https://ngrok.com/download)并在 PATH 中。
要求
- Node.js 24+
- macOS、Linux 或 Windows
openssl(用于--https证书生成;macOS 和大多数 Linux 发行版自带;在 Windows 上,通过winget install -e --id ShiningLight.OpenSSL.Dev安装,或使用 Git for Windows 附带的副本)tailscaleCLI(可选,用于--tailscale和--funnel)ngrokCLI(可选,用于--ngrok)






