2883 字
14 分钟
PortLoom 教程:用一台 VPS 把 NAS 服务发布到公网

家里的 NAS 跑了 Jellyfin、相册、下载工具和各种管理页面,但家庭宽带没有稳定公网入口。常见处理办法是手写 SSH 反向隧道,再配一层 Nginx 或 Caddy。服务少的时候还能维护,服务一多,端口、域名、隧道状态和配置文件很快就会乱。

PortLoom 把这套流程做成了一个 Web 控制台。公网 VPS 负责接收 HTTPS 请求,NAS 上的 Agent 主动连接 VPS,再把流量送回本地服务。NAS 不需要开放入站端口,也不用在路由器上逐个做端口转发。

本文基于当前的 v0.4 系列(官方新手模板固定使用经过验证的 0.4.1 镜像)。下面的域名和 IP 都是示例,部署时换成自己的值。

PortLoom 的工作方式#

PortLoom 需要两台能运行 Docker Compose 的 Linux 主机:

位置安装内容用途
公网 VPSServer、受管 sshdWebUI、内置 HTTPS 入口、路由网关和隧道入口
NAS 或内网服务器Agent主动连接 VPS,并把请求转发给本地服务

v0.4 起 HTTPS 证书由 Server 内置的 edge 直接申请和续期(ACME),不再需要单独的 Caddy 容器。

一次访问大致会经过这条链路:

浏览器
│ HTTPS
公网 VPS
PortLoom Server(内置 HTTPS edge → Gateway)
│ SSH 反向隧道
内网 NAS
PortLoom Agent → Jellyfin / 相册 / 管理页面

Agent 只向外连接 VPS 的 HTTPS 和 SSH 端口。公网请求沿已经建立的加密隧道返回 NAS,因此家庭网络不需要公网 IPv4,也不需要把 NAS 的服务端口直接暴露出去。

PortLoom 以 HTTP/HTTPS 域名路由为主。v0.4 起也支持发布 TCP/UDP 端口路由(在路由里选择对应协议并填写 Public port,同时在 VPS 放行该端口),可以覆盖数据库、游戏服务器这类非 Web 服务。

开始前要准备什么#

1. 公网 VPS#

VPS 需要:

  • 一个稳定的公网 IP
  • Linux 系统
  • Docker Engine
  • Docker Compose v2
  • TCP 804432222 可以从公网访问

80/443 由 Server 内置的 HTTPS edge 处理(ACME HTTP-01 签发要求公网 80 可达,且本机 80/443 未被其他程序占用),2222 是 PortLoom 专用 SSH 隧道端口。

2. NAS 或内网服务器#

NAS 需要能运行 Docker Compose,并且可以主动访问 VPS 的 4432222 端口。NAS 端不需要放行公网入站端口。

3. 域名和 DNS#

假设:

  • VPS 公网 IP:203.0.113.10
  • PortLoom 管理域名:portloom.example.com(管理域名没有命名要求,根域名或任意子域名都可以,不要求 portloom. 前缀)
  • 准备发布的 Jellyfin 域名:jellyfin.example.com

添加两条 DNS 记录:

portloom.example.com A 203.0.113.10
*.example.com A 203.0.113.10

通配符记录不是强制的,也可以给每个服务单独添加 A 记录。使用 Cloudflare 时,第一次部署建议先设为“仅 DNS”(灰云),避免 SSH 端口和证书签发被代理状态干扰。

确认解析结果:

Terminal window
nslookup portloom.example.com
nslookup jellyfin.example.com

两次查询都应该返回 VPS 的公网 IP。

在 VPS 安装 PortLoom Server#

v0.4 官方推荐的新手路径是 Compose 模板:下载两个文件、修改两个值、启动项目,不需要先运行任何安装脚本,配置全程可见。

方式一:Compose 模板(推荐)#

在 VPS 上创建一个专用目录(例如 portloom/),从官方文档下载 compose.yml 和环境变量模板,把 compose.env.example 重命名为 .env,放在同一目录:

portloom/
├── compose.yml
└── .env

打开 .env,只需要填两个必填值:

  • TM_PUBLIC_HOST:你的管理域名,例如 portloom.example.com(不要带 https://、端口或路径)
  • TM_ADMIN_TOKEN:终端运行 openssl rand -hex 32,把生成的 64 位十六进制串粘贴进去,它就是 WebUI 的登录凭证

两个值留空时 Compose 会拒绝启动,避免带着占位凭证误上线。

WARNING

.env 里的管理员 Token 相当于控制台密码。不要提交到 Git、放进截图或发给别人。建议同时保存到密码管理器。

然后在该目录执行:

Terminal window
chmod 0711 .
chmod 0600 .env
docker compose config --quiet
docker compose pull
docker compose up -d
docker compose ps -a

正常会看到:

  • portloom-server:运行中(WebUI、API、路由网关和内置 HTTPS edge)
  • portloom-sshd:运行中且健康(专门用于反向隧道的受限 SSH 服务)
  • state-init:成功退出——它只负责首次初始化数据目录权限,退出不是故障

群晖/QNAP 这类带 Compose 图形界面的系统,也可以直接在界面里选择这个目录新建项目启动,效果相同。

方式二:安全安装脚本#

如果希望自动生成随机凭证、固定不可变镜像并带失败回滚,可以用安装脚本(先下载查看内容再执行):

Terminal window
curl -fsSLo install-server.sh https://docs.look4i.com/install-server.sh
less install-server.sh
chmod 0700 install-server.sh
./install-server.sh \
--domain portloom.example.com \
--version 0.4.1

两种方式运行的是相同的 Server 和 sshd 镜像。

打开 WebUI#

容器正常后,在浏览器打开:

https://portloom.example.com

输入管理员 Token 登录(Compose 方式用 .env 里的 TM_ADMIN_TOKEN,脚本方式用终端显示的随机 Token)。第一次访问可能需要几十秒完成证书申请;页面能打开后,可以顺手验证健康端点:

Terminal window
curl --fail --head https://portloom.example.com/healthz

在 WebUI 添加 NAS#

登录后进入 Add Agent 页面,填写:

字段示例
Agent namehome-nas
Server URLhttps://portloom.example.com
Public Server hostportloom.example.com
SSH tunnel port2222

点击 Generate command。WebUI 会生成一条完整安装命令,其中包含一次性注册令牌和已经核验的 SSH 主机公钥。

这条命令不要自己拆开修改,也不要发到公开的地方。没执行过的安装命令可以在令牌列表里删除或撤销,已完成注册的 Agent 不受影响。

在 NAS 安装 Agent#

SSH 登录 NAS,把 WebUI 生成的完整命令原样执行。它会下载 install-agent.sh,并安装与 Server 相同版本的 Agent。

安装过程会自动完成这些事情:

  1. 生成独立 Ed25519 密钥
  2. 写入 Server 的 SSH 主机公钥
  3. 使用一次性令牌注册 Agent
  4. 上传 Agent 公钥并更新受限授权文件
  5. 建立反向隧道
  6. 注册成功后,从环境文件中删除一次性令牌

默认安装目录是:

~/.portloom/agent

查看 Agent 状态:

Terminal window
cd ~/.portloom/agent
docker compose --env-file .env -f compose.yml ps
docker compose --env-file .env -f compose.yml logs --tail=100 agent

回到 WebUI 的“客户端”页面,应该能看到 home-nas,并且心跳时间会持续更新。

发布第一个服务#

下面以 Jellyfin 为例。假设 Jellyfin 在 NAS 上监听 8096

进入 Routes → Add route,填写:

字段示例
NameJellyfin
Clienthome-nas
ProtocolHTTPS
Public domainjellyfin.example.com
Path prefix留空
Public port留空(使用主 HTTPS 入口)
Local host127.0.0.1
Local port8096

Agent 使用 host network,因此这里的 127.0.0.1 指 NAS 宿主机。如果目标服务在另一台局域网设备上,也可以填写对应的 LAN 地址,例如 192.168.1.20

如果前面没配 *.example.com 通配符解析,记得先把 jellyfin.example.com 单独解析到 VPS。

保存后,PortLoom 会分配 VPS 回环端口、建立 SSH 反向转发,并把域名加入内置 Gateway,证书自动申请。

等待状态收敛后访问:

https://jellyfin.example.com

如果页面能打开,说明第一条路由已经打通。

怎么看懂三层状态#

PortLoom 没有把所有问题压成一个绿色圆点,而是分成三层:

Local#

Agent 能否访问 local_host:local_port

如果 Local down,先在 NAS 上检查:

Terminal window
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 防火墙和 Server 日志(证书申请也在这里)。

常见故障怎么排#

按链路从近到远检查,通常比反复重装更快。

1. 控制台打不开#

进入 Server 的项目目录(Compose 模板方式就是你创建的 portloom/ 目录):

Terminal window
docker compose ps -a
docker compose logs --tail=100 server sshd

确认 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 保存配置。官方给的备份原则很明确:升级或备份前先停止 serversshd,备份整个项目目录——compose.yml、私密 .env 和完整的 data/,并记录 docker compose images 的输出;不要只复制运行中的 portloom.db 单个文件。

Agent 侧同理,~/.portloom/agent 里的数据目录(含 SSH 私钥、known_hostsagent.json)要一起备份,不要删除后重建。

升级前先看对应版本的 Release Notes。模板固定的是明确的镜像标签,不要在生产环境改成浮动的 latest。日常重启只操作长驻服务(docker compose restart server sshd);配置或镜像变化用 docker compose up -d,一次性的 state-init 不需要单独重启。

官方升级与回滚说明:

https://docs.look4i.com/operations/backup-upgrade

几个安全细节#

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 等家庭服务
  • 内网开发环境和测试页面
  • v0.4 起还包括数据库、游戏服务器等 TCP/UDP 服务

如果只发布一个临时页面,手写一条 SSH 隧道可能更快。服务多、域名多,或者希望在一个界面里看清本地、隧道和公网状态时,PortLoom 会省下不少重复配置。

项目和文档地址:

PortLoom 教程:用一台 VPS 把 NAS 服务发布到公网
https://blog.961121.xyz/posts/portloom-nas-public-access-tutorial/
作者
LOOK
发布于
2026-07-17
许可协议
CC BY-NC-SA 4.0