家里的 NAS 跑了 Jellyfin、相册、下载工具和各种管理页面,但家庭宽带没有稳定公网入口。常见处理办法是手写 SSH 反向隧道,再配一层 Nginx 或 Caddy。服务少的时候还能维护,服务一多,端口、域名、隧道状态和配置文件很快就会乱。
PortLoom 把这套流程做成了一个 Web 控制台。公网 VPS 负责接收 HTTPS 请求,NAS 上的 Agent 主动连接 VPS,再把流量送回本地服务。NAS 不需要开放入站端口,也不用在路由器上逐个做端口转发。
本文使用当前正式版 v0.2.1。下面的域名和 IP 都是示例,部署时换成自己的值。
PortLoom 的工作方式
PortLoom 需要两台能运行 Docker Compose 的 Linux 主机:
| 位置 | 安装内容 | 用途 |
|---|---|---|
| 公网 VPS | Server、专用 SSHD、Caddy | WebUI、HTTPS 入口、路由网关和隧道入口 |
| NAS 或内网服务器 | Agent | 主动连接 VPS,并把请求转发给本地服务 |
一次访问大致会经过这条链路:
浏览器 │ HTTPS ▼公网 VPS Caddy → PortLoom Gateway │ │ SSH 反向隧道 ▼内网 NAS PortLoom Agent → Jellyfin / 相册 / 管理页面Agent 只向外连接 VPS 的 HTTPS 和 SSH 端口。公网请求沿已经建立的加密隧道返回 NAS,因此家庭网络不需要公网 IPv4,也不需要把 NAS 的服务端口直接暴露出去。
PortLoom 当前完整管理的是 HTTP/HTTPS 域名路由。它不会替你发布任意原始 TCP 端口。如果要暴露数据库、游戏端口或其他 TCP 服务,需要另找合适的方案。
开始前要准备什么
1. 公网 VPS
VPS 需要:
- 一个稳定的公网 IP
- Linux 系统
- Docker Engine
- Docker Compose v2
- TCP
80、443和2222可以从公网访问
80/443 由 Caddy 处理 HTTPS,2222 是 PortLoom 专用 SSH 隧道端口。
2. NAS 或内网服务器
NAS 需要能运行 Docker Compose,并且可以主动访问 VPS 的 443 和 2222 端口。NAS 端不需要放行公网入站端口。
3. 域名和 DNS
假设:
- VPS 公网 IP:
203.0.113.10 - PortLoom 管理域名:
portloom.example.com - 准备发布的 Jellyfin 域名:
jellyfin.example.com
添加两条 DNS 记录:
portloom.example.com A 203.0.113.10*.example.com A 203.0.113.10通配符记录不是强制的,也可以给每个服务单独添加 A 记录。使用 Cloudflare 时,第一次部署建议先设为“仅 DNS”(灰云),避免 SSH 端口和证书签发被代理状态干扰。
确认解析结果:
nslookup portloom.example.comnslookup jellyfin.example.com两次查询都应该返回 VPS 的公网 IP。
在 VPS 安装 PortLoom Server
先下载脚本并查看内容,再执行安装。这里固定使用 0.2.1,避免日后 latest 更新后教程与实际版本对不上。
curl -fsSLo install-server.sh https://docs.961121.xyz/install-server.shless install-server.shchmod 0700 install-server.sh./install-server.sh \ --domain portloom.example.com \ --version 0.2.1安装器会启动三个容器:
portloom-server:WebUI、API 和路由网关portloom-sshd:专门用于反向隧道的受限 SSH 服务portloom-caddy:自动申请和续期 HTTPS 证书
默认安装目录是:
~/.portloom/server安装完成后,终端会显示:
- WebUI 地址
- 随机生成的管理员 Token
- 配置文件目录
WARNING管理员 Token 相当于控制台密码。不要放进截图、博客、聊天记录或公开仓库。建议立即保存到密码管理器,服务器上的
.env也要保持私有。
用下面的命令查看容器状态:
cd ~/.portloom/serverdocker compose --env-file .env -f compose.yml ps三个容器都正常后,在浏览器打开:
https://portloom.example.com输入安装器输出的管理员 Token 登录。
在 WebUI 添加 NAS
登录后进入“添加代理”页面,填写:
| 字段 | 示例 |
|---|---|
| 代理名称 | home-nas |
| Server URL | https://portloom.example.com |
| 公网 Server 主机 | portloom.example.com |
| SSH 隧道端口 | 2222 |
点击“生成命令”。WebUI 会生成一条完整安装命令,其中包含一次性注册令牌和已经核验的 SSH 主机公钥。
这条命令只显示一次,不要自己拆开修改,也不要把它发到公开的地方。
在 NAS 安装 Agent
SSH 登录 NAS,把 WebUI 生成的完整命令原样执行。它会下载 install-agent.sh,并安装与 Server 相同版本的 Agent。
安装过程会自动完成这些事情:
- 生成独立 Ed25519 密钥
- 写入 Server 的 SSH 主机公钥
- 使用一次性令牌注册 Agent
- 上传 Agent 公钥并更新受限授权文件
- 建立反向隧道
- 注册成功后,从环境文件中删除一次性令牌
默认安装目录是:
~/.portloom/agent查看 Agent 状态:
cd ~/.portloom/agentdocker compose --env-file .env -f compose.yml psdocker compose --env-file .env -f compose.yml logs --tail=100 agent回到 WebUI 的“客户端”页面,应该能看到 home-nas,并且心跳时间会持续更新。
发布第一个服务
下面以 Jellyfin 为例。假设 Jellyfin 在 NAS 上监听 8096。
进入“路由”,点击“添加 HTTP 路由”,填写:
| 字段 | 示例 |
|---|---|
| 名称 | Jellyfin |
| 客户端 | home-nas |
| 协议 | HTTP |
| 公网域名 | jellyfin.example.com |
| 本地主机 | 127.0.0.1 |
| 本地端口 | 8096 |
Agent 使用 host network,因此这里的 127.0.0.1 指 NAS 宿主机。如果目标服务在另一台局域网设备上,也可以填写对应的 LAN 地址,例如 192.168.1.20。
保存后,PortLoom 会分配 VPS 回环端口、建立 SSH 反向转发,并把域名加入 Gateway。Caddy 只会为 WebUI 中已经启用的 HTTP 路由提供入口。
等待状态收敛后访问:
https://jellyfin.example.com如果页面能打开,说明第一条路由已经打通。
怎么看懂三层状态
PortLoom 没有把所有问题压成一个绿色圆点,而是分成三层:
Local
Agent 能否访问 local_host:local_port。
如果 Local down,先在 NAS 上检查:
curl -I http://127.0.0.1:8096如果这里就不通,问题通常在本地服务、监听地址、端口或 NAS 防火墙,与 VPS 和域名无关。
Tunnel
SSH 反向转发是否已经建立。Tunnel down 常见原因包括:
- NAS 无法访问 VPS 的
2222 - SSH 密钥或
known_hosts权限不正确 - VPS 防火墙没有放行
2222 - 远程端口冲突
Public
Agent 是否已经应用最新配置,以及内置 Gateway 是否已经收敛。
Public 显示 published,不代表外部 DNS 和 TLS 一定正确。如果 Local、Tunnel、Public 都正常,但公网仍打不开,就继续检查域名解析、VPS 的 80/443 防火墙和 Caddy 日志。
常见故障怎么排
按链路从近到远检查,通常比反复重装更快。
1. 控制台打不开
cd ~/.portloom/serverdocker compose --env-file .env -f compose.yml psdocker compose --env-file .env -f compose.yml logs --tail=100 serverdocker compose --env-file .env -f compose.yml logs --tail=100 caddy确认 DNS 指向 VPS,并检查 80/443 是否放行。
2. Agent 无法注册
检查:
- 一次性令牌是否已经使用或过期
- NAS 和 VPS 的系统时间是否正确
- NAS 是否能访问管理域名的 HTTPS
- Agent 数据目录是否持久化且可写
注册命令失败后不要删除 Agent 状态目录。修好网络后,先重新执行同一条安装命令,安装器会尝试安全恢复。
3. Gateway 返回 404
一般是 Host 没有匹配到路由。检查公网域名是否填写正确、路由是否启用,以及请求使用的域名是否和 WebUI 完全一致。
4. Gateway 返回 502
这通常表示 VPS 侧没有可用的隧道监听。先看 Tunnel 状态和 Agent 日志,不要先删除路由或数据库。
5. 页面能开,但视频或大文件异常
首页返回 200 只说明基础链路可用。媒体服务还要验证:
- 大文件连续下载
- HTTP Range 请求
- WebSocket(如果应用需要)
- 长连接
- 实际上行带宽
备份和升级
Server 使用 SQLite 保存配置。零停机备份时应该使用 SQLite 在线备份 API;如果直接复制数据库文件,应先停止 Server,或者同时一致地保存数据库、WAL 和 SHM 文件。
至少备份这些内容:
- Server 数据目录
- Agent 的
data/agent.json - Agent SSH 私钥和
known_hosts - Server 与 Agent 的
.env、compose.yml
升级前先看对应版本的 Release Notes,并固定明确的镜像标签。不要在生产环境长期依赖浮动的 latest。一次只升级一个组件,先 Server,再普通 Web Agent,最后处理高流量 Agent;每一步都检查心跳、Revision 和公网请求。
官方升级与回滚说明:
几个安全细节
PortLoom 已经把 Server、SSHD 和 Agent 拆开,并对容器做了非 root 运行、只读根文件系统和能力裁剪,但部署者仍需要保护好凭据:
- 不要公开管理员 Token
- 不要把一次性注册命令发给别人
.env、SSH 私钥和备份目录使用严格权限- 不要让 PortLoom 容器访问 Docker Socket
- 管理域名只使用 HTTPS
- 删除路由或重建数据前先备份
- 不要把删除 SQLite、Agent 状态目录或重新注册当成首选排错手段
PortLoom 的管理域名同时承载 Agent API。如果还要叠加来源 IP 限制、VPN 或身份验证网关,必须保证 NAS 仍能访问 Agent 所需的 API;不要直接给整个域名套一层只适合浏览器的登录页,否则心跳和配置同步也会被拦截。
适合哪些场景
PortLoom 适合把这些 Web 服务发布出去:
- Jellyfin、Emby 等媒体服务
- 自建博客和静态站点
- NAS 相册、文件管理页面
- Home Assistant 等家庭服务
- 内网开发环境和测试页面
如果只发布一个临时页面,手写一条 SSH 隧道可能更快。服务多、域名多,或者希望在一个界面里看清本地、隧道和公网状态时,PortLoom 会省下不少重复配置。
项目和文档地址:
- GitHub:https://github.com/lkhmm520/portloom
- 中文文档:https://docs.961121.xyz/
- v0.2.1 Release:https://github.com/lkhmm520/portloom/releases/tag/v0.2.1