希曼日记

把家里的 Mac 接到公网:Pangolin 服务端部署与 Newt 自启动实战

约 18 分钟6,871 字

家里的 Mac 上有一个 Web 服务,自己在浏览器里打开 http://127.0.0.1:3000 就能用。离开家以后,我希望通过自己的域名访问它,但不想给家里的路由器配置端口转发,也不想把开发服务直接监听到公网。

我的做法是:在公网 VPS 上部署 Pangolin,在 Mac 上运行 Newt,由 Mac 主动建立隧道。外部访问先到 VPS,经过 HTTPS 和访问认证,再沿隧道到达 Mac 本地服务。

最近重装 macOS,原来的 mac 站点还在,Newt 的本地配置却没了。重新接入后,又需要把终端里的前台进程改成登录后自动运行。这篇文章把服务端安装、Mac 接入和长期运行整理到一起,不再只记录一条启动命令。

本文参考现有的 docker-deploy/pangolin/ 部署,整理为一套独立示例:去掉 Vault 专用网络、预留的 raw 端口和与本例无关的配置,补齐压缩中间件定义。示例域名统一使用 example.com,不包含实际站点 ID、Secret、服务器密钥或证书私钥。此前的排错过程见《Pangolin 内网穿透:一个 502 背后叠着四个坑》。

一、先分清服务端、站点和资源

Text
Mac 主动连接:
Mac / Newt ── HTTPS 控制连接 + WireGuard 隧道 ──→ 公网 VPS
用户访问:
浏览器 ── HTTPS ──→ VPS / Traefik ── 隧道 ──→ Mac / Newt
│ │
Pangolin 资源认证 127.0.0.1:3000
│ │
Gerbil 隧道端 本地 Web 服务
组件或概念部署位置作用
PangolinVPS管理面板、站点、资源、权限和路由配置
GerbilVPS隧道服务端;本例的公网端口都发布在这个容器上
TraefikVPSHTTPS 入口、证书和反向代理,与 Gerbil 共享网络命名空间
NewtMac站点连接器,主动连接服务端,并访问本地目标
Site(站点)Pangolin 中的配置表示 Mac 这一侧的连接,不等于已经发布了服务
Resource(资源)Pangolin 中的配置把一个外部域名映射到指定站点和目标端口,并设置访问权限

Mac 不需要再部署一套 Pangolin 服务端,只需要安装 Newt。 这里的 Newt 是站点连接器,不是用于访问私有资源的 Pangolin 桌面客户端。名字也不是 netw。

本文只发布一个 HTTP 服务,不配置整网段访问,也不公开 SSH、数据库或 macOS 远程管理端口。站点在线并不意味着 VPS 获得了 Mac 任意端口的访问入口。

本文使用的地址

项目示例值
VPS一台具有公网 IP 的 Linux 服务器,运维时可通过 SSH 登录
Pangolin 管理地址https://pangolin.example.com
对外资源地址https://demo.example.com
Mac 站点名称mac
Mac 上的目标服务http://127.0.0.1:3000
VPS 部署目录/home/docker/pangolin
Mac 程序路径$HOME/.local/bin/newt

后文所有 example.com 都要替换成自己的域名。Newt 的 Endpoint 填管理地址,不能填 https://demo.example.com。

二、准备 VPS、DNS 和证书

版本基线与前置条件

本例沿用现有部署中的版本组合,不声称它们是最新版本:

组件版本
Pangolin CE1.22.2
Gerbil1.5.0
Traefikv3.7.12
Badger 认证插件v1.7.0
Newt本次 Mac 接入使用 1.16.0

新部署前检查官方发布说明,按配套关系选择版本,不要把几个镜像分别改为 latest。本文的手动配置以这组版本为基线;新版本的字段、菜单和迁移行为可能不同。

VPS 需要满足以下条件:

  • 已安装 Docker Engine 和 Compose 插件,可执行 docker compose。尚未安装时,按Docker 官方 Ubuntu 安装文档或对应发行版文档完成安装。
  • Linux 内核支持 WireGuard,容器允许使用 NET_ADMIN;受限容器型 VPS 需要先确认支持情况。
  • 服务器能访问镜像仓库,以及 Traefik 下载 Badger 插件所需的网络地址。
  • 80、443 端口没有被现有 Nginx、Caddy 或另一套 Traefik 占用。已有公网入口时,应另行设计共存方案,不要直接停掉生产服务。

后文 VPS 命令在有 Docker 权限的管理终端中执行,目录创建和证书安装需要相应文件权限。先检查:

Bash
docker version
docker compose version
sudo ss -lntup

如果 docker version 只有客户端信息、无法连接服务端,先修复 Docker 服务或权限,再继续。

配置 DNS

在 DNS 服务商处添加记录:

类型主机记录指向
ApangolinVPS 公网 IPv4
AdemoVPS 公网 IPv4

需要频繁增加同级子域名时,可以用 * 的 A 记录代替逐条添加资源记录,但仍建议显式保留 pangolin。泛解析不等于根域解析,也不会覆盖已经存在的同名记录。

本教程先使用直接解析、仅 DNS 模式,不叠加 CDN 代理。不要添加不可用的 AAAA 记录,否则客户端可能优先走向不通的 IPv6 地址。

放行公网端口

同时检查云安全组和服务器自身防火墙:

端口用途本例处理
80/tcpHTTP 入口与管理域名跳转放行
443/tcp面板、控制连接和资源 HTTPS放行
51820/udp站点 WireGuard 隧道放行
21820/udpGerbil 用于客户端连接的 WireGuard 端口保留映射并放行;本文不演示私有客户端接入
443/udpHTTP/3本例不启用,不需要开放
3000、3001、3002、3004、8080容器内部服务与管理接口不发布到公网

家里的路由器不需要配置入站端口转发,但 Mac 所在网络需要允许相关出站连接。能打开 Pangolin 面板只证明 HTTPS 可达,不能证明 UDP 隧道可用。

不要为了排查隧道而关闭整台 VPS 的防火墙。Docker 的端口发布与主机防火墙规则存在交互,不能只看 UFW 的规则列表就认定端口未暴露;云安全组也要按实际需要限制。

准备 HTTPS 证书

现有部署使用自备证书(BYO),本文沿用这个方案,不混入另一套 ACME 自动申请配置。准备一张受信任的证书,至少覆盖:

  • pangolin.example.com
  • demo.example.com

也可以使用包含 *.example.com 的泛域名证书;如果还要服务根域,再包含 example.com。泛域名证书一般需要通过 ACME DNS-01 验证申请,具体 DNS API 凭据与权限参考acme.sh DNS API 文档。DNS Token 和证书私钥都不能提交到博客或公开仓库。

后文的 example.com.crt 必须是包含中间证书的完整证书链,不只是叶子证书;example.com.key 是配套私钥。

如果不准备自行管理证书,优先使用Pangolin 官方安装器生成 ACME 方案,不要与本文关闭 ACME 的配置混用。

三、部署 Pangolin 服务端

以下是新安装流程。已有实例不要覆盖配置,尤其不要重新生成 server.secret。

1. 创建目录

在 VPS 上执行:

Bash
sudo mkdir -p /home/docker/pangolin/config/traefik/certs
sudo mkdir -p /home/docker/pangolin/config/letsencrypt
cd /home/docker/pangolin

接下来用编辑器创建四份文件:

Text
/home/docker/pangolin/
├── docker-compose.yml
└── config/
├── config.yml
├── letsencrypt/
└── traefik/
├── traefik_config.yml
├── dynamic_config.yml
└── certs/
├── example.com.crt
└── example.com.key

config/db/ 和 Gerbil 密钥等运行数据会在启动后生成,需要一起持久化和备份。

2. 编写 docker-compose.yml

保存为 /home/docker/pangolin/docker-compose.yml:

YAML
name: pangolin
services:
pangolin:
image: docker.io/fosrl/pangolin:1.22.2
container_name: pangolin
restart: unless-stopped
volumes:
- ./config:/app/config
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
interval: 10s
timeout: 10s
retries: 15
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
gerbil:
image: docker.io/fosrl/gerbil:1.5.0
container_name: gerbil
restart: unless-stopped
depends_on:
pangolin:
condition: service_healthy
command:
- --reachableAt=http://gerbil:3004
- --generateAndSaveKeyTo=/var/config/key
- --remoteConfig=http://pangolin:3001/api/v1/
volumes:
- ./config:/var/config
cap_add:
- NET_ADMIN
- SYS_MODULE
ports:
- "80:80/tcp"
- "443:443/tcp"
- "51820:51820/udp"
- "21820:21820/udp"
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
traefik:
image: docker.io/traefik:v3.7.12
container_name: traefik
restart: unless-stopped
network_mode: service:gerbil
depends_on:
pangolin:
condition: service_healthy
gerbil:
condition: service_started
command:
- --configFile=/etc/traefik/traefik_config.yml
volumes:
- ./config/traefik:/etc/traefik:ro
- ./config/letsencrypt:/letsencrypt
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
networks:
default:
name: pangolin_frontend
driver: bridge

注意两点:

  1. Traefik 使用 network_mode: service:gerbil,所以公网端口写在 Gerbil 上,不能再给 Traefik 单独写一份 ports。
  2. 本例不依赖 vault_backend 等外部网络,首次部署无需先安装 Vault;也没有预留数据库或 SSH 的公网端口。

3. 编写 Pangolin 配置

先在 VPS 上生成服务端密钥:

Bash
openssl rand -hex 32

把输出填入下面的 server.secret。这个值不是 Mac 的 Newt Secret:前者属于 Pangolin 服务端,后者属于某个站点,不能混用。

保存为 /home/docker/pangolin/config/config.yml:

YAML
gerbil:
start_port: 51820
base_endpoint: "pangolin.example.com"
app:
dashboard_url: "https://pangolin.example.com"
log_level: "info"
telemetry:
anonymous_usage: false
domains:
domain1:
base_domain: "example.com"
server:
secret: "REPLACE_WITH_OPENSSL_OUTPUT"
cors:
origins: ["https://pangolin.example.com"]
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
allowed_headers: ["X-CSRF-Token", "Content-Type"]
credentials: false
traefik:
cert_resolver: ""
flags:
require_email_verification: false
disable_signup_without_invite: true
disable_user_create_org: false
allow_raw_resources: false
allow_base_domain_resources: false
enable_acme_cert_sync: false

本例只使用子域名 HTTP 资源,因此关闭 raw 资源和根域资源。disable_signup_without_invite 限制面板的公开注册,不等于所有已发布资源都自动配置好了访问权限,资源认证还要单独检查。

server.secret 涉及已有数据和认证,实例投入使用后不要把更换它当成普通配置修改;确需轮换时,遵循对应版本的官方流程。

4. 编写 Traefik 静态配置

保存为 /home/docker/pangolin/config/traefik/traefik_config.yml:

YAML
providers:
http:
endpoint: "http://pangolin:3001/api/v1/traefik-config"
pollInterval: "5s"
file:
filename: "/etc/traefik/dynamic_config.yml"
watch: true
experimental:
plugins:
badger:
moduleName: "github.com/fosrl/badger"
version: "v1.7.0"
log:
level: "INFO"
format: "common"
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
transport:
respondingTimeouts:
readTimeout: "30m"
http:
tls: {}
middlewares:
- compress@file
ping:
entryPoint: web

这份静态配置通过 Pangolin 的内部接口获取资源路由,通过 file provider 获取面板路由和证书。没有启用 Traefik 的不安全管理接口,也没有设置全局 insecureSkipVerify;本文的本地目标是 HTTP,不需要跳过上游 TLS 校验。

compress@file 必须在下一份文件中定义,不能只引用名字。若日后发布 SSE 等流式接口,应单独验证缓冲和压缩行为,再决定是否为这类资源移除压缩。

5. 编写路由和证书配置

保存为 /home/docker/pangolin/config/traefik/dynamic_config.yml:

YAML
http:
middlewares:
badger:
plugin:
badger:
disableForwardAuth: true
redirect-to-https:
redirectScheme:
scheme: https
compress:
compress: {}
routers:
main-app-router-redirect:
rule: "Host(`pangolin.example.com`)"
service: next-service
entryPoints:
- web
middlewares:
- redirect-to-https
- badger
next-router:
rule: "Host(`pangolin.example.com`) && !PathPrefix(`/api/v1`)"
service: next-service
entryPoints:
- websecure
middlewares:
- badger
tls: {}
api-router:
rule: "Host(`pangolin.example.com`) && PathPrefix(`/api/v1`)"
service: api-service
entryPoints:
- websecure
middlewares:
- badger
tls: {}
ws-router:
rule: "Host(`pangolin.example.com`)"
service: api-service
entryPoints:
- websecure
middlewares:
- badger
tls: {}
services:
next-service:
loadBalancer:
servers:
- url: "http://pangolin:3002"
api-service:
loadBalancer:
servers:
- url: "http://pangolin:3000"
tls:
certificates:
- certFile: /etc/traefik/certs/example.com.crt
keyFile: /etc/traefik/certs/example.com.key
stores:
default:
defaultCertificate:
certFile: /etc/traefik/certs/example.com.crt
keyFile: /etc/traefik/certs/example.com.key

这里配置的是管理面板入口,不需要手工再写一条 demo.example.com 路由;资源路由由 Pangolin 根据面板配置动态下发。

6. 安装证书并启动

把以下源路径替换为实际证书路径,在 VPS 上执行:

Bash
cd /home/docker/pangolin
sudo install -m 644 /path/to/fullchain.pem config/traefik/certs/example.com.crt
sudo install -m 600 /path/to/privkey.pem config/traefik/certs/example.com.key
sudo chmod 600 config/config.yml

启动前逐项确认:

  • 四份文件中的域名已经替换,证书文件名与 YAML 路径一致。
  • server.secret 已换成随机值,不是占位符。
  • 证书覆盖管理域名与资源域名,私钥与证书匹配。
  • DNS 和安全组已配置,没有重复占用公网端口。

然后执行:

Bash
cd /home/docker/pangolin
docker compose config --quiet
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 pangolin gerbil traefik

config --quiet 只检查 Compose 结构,不会验证证书、Pangolin 配置或 Traefik 路由是否正确。容器启动后还需要看日志,并在浏览器中打开:

Text
https://pangolin.example.com/auth/initial-setup

按页面提示完成初始管理员设置;若要求 setup token,从 docker compose logs pangolin 中获取,不要公开粘贴日志中的令牌。随后创建或进入组织,检查 example.com 域名是否可用于创建资源。

浏览器应建立可信的 HTTPS 连接。证书报错时先修复证书链、域名或有效期,不要用永久忽略证书错误作为解决方案。自备证书场景下,面板中的证书状态可能不能反映实际 TLS 状态,应以真实握手为准。

四、在 Mac 安装 Newt 并接入站点

以下命令在 Mac 执行,不是在 VPS 中执行。

1. 安装原生 Newt

为便于复现,下面固定安装本次接入使用的 1.16.0,自动选择 Apple Silicon 或 Intel 二进制,并拒绝覆盖已有文件:

Bash
/bin/bash <<'BASH'
set -euo pipefail
VERSION=1.16.0
case "$(uname -m)" in
arm64) ARCH=arm64 ;;
x86_64) ARCH=amd64 ;;
*) echo "不支持的架构,请检查官方发布文件。"; exit 1 ;;
esac
mkdir -p "$HOME/.local/bin"
DEST="$HOME/.local/bin/newt"
if [ -e "$DEST" ]; then
echo "Newt 已存在,未覆盖:$DEST"
"$DEST" --version
exit 0
fi
TEMP_FILE=$(mktemp)
trap 'rm -f "$TEMP_FILE"' EXIT
curl -fL "https://github.com/fosrl/newt/releases/download/${VERSION}/newt_darwin_${ARCH}" \
-o "$TEMP_FILE"
install -m 755 "$TEMP_FILE" "$DEST"
"$DEST" --version
BASH

下载地址来自 Newt 官方发布页。高安全要求环境还应核验发布方提供的校验信息,再运行二进制。

如果希望安装当前最新版本,官方安装文档提供以下脚本方式。它会下载并执行远程脚本,应先审查脚本,并确认新版本兼容性;它是上面固定版本安装的替代方案,不需要再执行一次。

Bash
mkdir -p "$HOME/.local/bin"
curl -fsSL https://static.pangolin.net/get-newt.sh \
| bash -s -- --path "$HOME/.local/bin/newt"

不要直接运行 brew install newt:Homebrew 中的这个同名包不是 Pangolin 的站点连接器。本文始终使用绝对路径调用,也不依赖 shell 的 PATH。

2. 创建或恢复 mac 站点

在 Pangolin 面板进入 Sites → Add Site,创建名为 mac 的 Newt 站点,保存连接凭据。不同版本的按钮名称可能略有差异,核心是获取以下三个值:

参数来源
Endpoint本例为 https://pangolin.example.com
Newt ID站点创建或凭据页面提供的连接器 ID,不是站点名称,也不是数据库里的数字 siteId
Newt Secret站点连接密钥,不是管理员密码,也不是 server.secret

如果只是重装 Mac,原站点仍然存在,先恢复原站点,不要直接删除它。已经绑定的资源可以继续使用原站点。

服务端存储的是 Newt Secret 的哈希,不能把哈希拿来启动 Newt,也不能从哈希还原明文。 丢失凭据时,进入 Sites → mac → Credentials,使用官方提供的重新生成操作;如果旧设备或旧会话也需要立即失效,选择 Regenerate and Disconnect。重新生成后以页面给出的整组 ID 和 Secret 为准,不要假定 ID 一定不变。

本次重装恢复时曾通过服务端备份数据库并修改指定站点哈希应急处理,但这不适合作为通用安装步骤:数据库结构与会话逻辑依版本变化,读者应优先使用官方凭据轮换入口。

3. 前台连接,先验证再做自启动

将占位值替换为站点页面提供的值:

Bash
"$HOME/.local/bin/newt" \
--endpoint "https://pangolin.example.com" \
--id "YOUR_NEWT_ID" \
--secret "YOUR_NEWT_SECRET"

参数形式便于理解,但真实 Secret 可能留在 shell 历史和进程参数中。不要分享这条真实命令;后文 launchd 使用受限权限的 plist 和环境变量,避免把 Secret 写进程序参数,但它仍不是加密存储。

保持终端运行,到 Pangolin 页面确认 mac 站点在线。如果认证失败,先检查 Endpoint、ID、Secret 是否属于同一实例和站点;如果控制连接正常但隧道不通,再查 UDP 网络和 Gerbil 日志。

五、把 Mac 本地服务发布为 HTTPS 资源

1. 准备一个不含隐私数据的测试服务

Mac 需要 Python 3 来运行本节测试服务和下一节 plist 生成器。先检查:

Bash
python3 --version

如果没有 Python 3,可以通过已有的 Homebrew 执行 brew install python,或从 Python 官网安装。不要假定每台 macOS 都自带可用的 Python 3。

另开一个 Mac 终端,在专门的空目录中运行:

Bash
mkdir -p "$HOME/pangolin-demo"
printf '%s\n' 'Hello from my Mac via Pangolin' > "$HOME/pangolin-demo/index.html"
python3 -m http.server 3000 --bind 127.0.0.1 --directory "$HOME/pangolin-demo"

不要在主目录、代码仓库或包含配置密钥的目录中启动这个文件服务器。它只是验收用的临时服务,不是生产 Web 服务器。

在另一个终端检查:

Bash
curl -fsS http://127.0.0.1:3000/

预期看到 Hello from my Mac via Pangolin。本地都无法访问时,先修复服务,不要继续调整公网路由。

2. 创建资源并配置访问权限

在 Pangolin 的 Resources 中创建 HTTP 资源,填写:

字段值
名称mac-demo
域名 / 子域名example.com / demo
站点mac
目标协议HTTP
目标地址127.0.0.1
目标端口3000
目标状态启用
访问权限启用 Pangolin 认证,并允许自己的用户或角色访问

这里外部是 HTTPS,目标是 HTTP,两者并不矛盾:TLS 在 VPS 的 Traefik 终结,跨网络的传输经过加密隧道,Newt 再连接 Mac 的 loopback 服务。

127.0.0.1 的含义取决于谁访问目标。 本例是 Mac 上原生运行的 Newt,所以这个地址指向 Mac。其他部署方式不能直接照抄:

场景目标地址的含义
原生 Mac Newt → Mac 服务可以用 127.0.0.1:3000
Docker Desktop 中的 Newt → Mac 服务容器的 127.0.0.1 不是 Mac;需要考虑 host.docker.internal 和宿主监听范围,另行验证
VPS 的 local site → VPS 容器使用可达的容器名和容器端口,通常需要同一个 Docker 网络

0.0.0.0 是服务监听地址,不是应该填进资源的目标地址。本文也不需要把 Mac 服务改为监听 0.0.0.0。

3. 从外部网络验收

资源配置下发需要一定时间,本例 Traefik 每 5s 轮询一次。稍后从手机蜂窝网络或另一台外部设备打开:

Text
https://demo.example.com

验收分三步:

  1. 未认证访问时,应受到 Pangolin 访问控制,而不是直接看到页面。
  2. 使用被授权账号登录后,应看到测试文本。
  3. 在 Mac 停止测试 HTTP 服务后,资源不应还能从其他错误目标返回同样的内容;恢复服务后再次确认可用。

curl -I https://demo.example.com 可能返回认证相关的 401 或跳转响应,不能因此认定隧道故障。Badger 的行为可能因版本、User-Agent 和认证状态不同而变化,应结合浏览器和日志判断,不要为了拿到 200 就关闭认证。

六、用 launchd 让 Newt 登录后自动运行

LaunchAgent 的边界

本文使用用户级 LaunchAgent:当前用户登录图形桌面后自动启动,进程退出后自动拉起,不需要 sudo。它并不是系统启动后、无人登录时就运行的 LaunchDaemon。

能力本文配置是否提供
登录 macOS 后自动启动 Newt是
关闭终端后继续运行是
Newt 进程退出后自动重启是
注销用户后继续运行否
机器重启后停在登录界面时运行否
阻止系统休眠或合盖休眠否
自动启动被代理的 Web 应用否

如果 Mac 是无人值守服务器,需要登录前运行,应另行配置 /Library/LaunchDaemons/ 下的系统级任务、运行用户、文件所有权和权限。它也不能绕过 FileVault 的开机解锁,更不能在机器休眠时转发流量。

1. 停止旧的前台 Newt

在前台 Newt 终端按 Ctrl+C。同一组站点凭据只保留一个 Newt 实例,不要同时运行手工进程、Docker 容器和 launchd 任务。

这里停止的是 Newt,不是第五节的测试 Web 服务;首次验收自启动时,测试服务仍需保持运行。

2. 安全生成 plist

以下程序在 Mac 运行,依次提示输入 Endpoint、Newt ID 和 Secret。Secret 输入不会回显,也不会直接成为 shell 历史里的命令。

生成器使用 Python 的 plistlib,避免手写 XML 时因路径或特殊字符产生转义错误。它拒绝覆盖已有 plist;如果已经配置过前面的版本,直接检查和管理现有任务即可,不要重复创建。

Bash
python3 <<'PY'
import getpass
import os
from pathlib import Path
import plistlib
from urllib.parse import urlparse
home = Path.home()
binary = home / ".local/bin/newt"
plist = home / "Library/LaunchAgents/net.pangolin.newt.plist"
logs = home / "Library/Logs/Newt"
if not binary.is_file() or not os.access(binary, os.X_OK):
raise SystemExit(f"Newt 不存在或不可执行:{binary}")
if plist.exists():
raise SystemExit(f"配置已存在,未覆盖:{plist}")
# 脚本从标准输入读取,交互凭据单独从终端读取。
with open("/dev/tty", "r") as tty_in, open("/dev/tty", "w") as tty_out:
def ask(prompt):
tty_out.write(prompt)
tty_out.flush()
return tty_in.readline().strip()
endpoint = ask("Pangolin Endpoint(https://...): ")
newt_id = ask("Newt ID: ")
secret = getpass.getpass("Newt Secret(不回显): ", stream=tty_out)
parsed = urlparse(endpoint)
if parsed.scheme != "https" or not parsed.netloc:
raise SystemExit("Endpoint 必须是完整的 HTTPS 管理地址。")
if not newt_id or not secret:
raise SystemExit("Newt ID 和 Secret 不能为空。")
config = {
"Label": "net.pangolin.newt",
"ProgramArguments": [str(binary)],
"EnvironmentVariables": {
"PANGOLIN_ENDPOINT": endpoint,
"NEWT_ID": newt_id,
"NEWT_SECRET": secret,
},
"RunAtLoad": True,
"KeepAlive": True,
"ThrottleInterval": 10,
"StandardOutPath": str(logs / "stdout.log"),
"StandardErrorPath": str(logs / "stderr.log"),
}
os.umask(0o077)
plist.parent.mkdir(parents=True, exist_ok=True)
logs.mkdir(parents=True, exist_ok=True)
logs.chmod(0o700)
with plist.open("xb") as output:
plistlib.dump(config, output)
plist.chmod(0o600)
print(f"已生成:{plist}")
PY

关键字段的作用:

字段用途
ProgramArguments使用 Newt 的绝对路径,不依赖 .zshrc 或 PATH
EnvironmentVariables给 Newt 传入 Endpoint、ID 和 Secret,不依赖终端导出的变量
RunAtLoad加载任务时启动
KeepAlive进程退出后由 launchd 再次启动
ThrottleInterval限制频繁拉起,避免配置错误时高速重启;不是网络重连超时
StandardOutPath / StandardErrorPath保存标准输出和错误日志

plist 中保存的是生成时展开的绝对路径。launchd 不会像 shell 那样自动展开手写的 ~ 或 $HOME,也不会替你执行 source ~/.zshrc。

环境变量并非秘密保险箱。 plist 仍以明文保存凭据;600 限制普通文件访问,但当前用户、管理员以及部分诊断输出仍可能读到。不要分享 plist,也不要公开粘贴完整的 launchctl print 输出。把凭据备份到密码管理器,而不是 Git。

3. 检查配置并加载

先验证 plist:

Bash
plutil -lint "$HOME/Library/LaunchAgents/net.pangolin.newt.plist"

看到 OK 后,在当前已登录的 Mac 用户终端执行:

Bash
launchctl bootstrap "gui/$(id -u)" \
"$HOME/Library/LaunchAgents/net.pangolin.newt.plist"

加载后会立即启动,以后登录时自动启动。现在可以关闭原来用于管理 Newt 的终端。

如果此前手工禁用过同名任务,需要先恢复启用,再加载:

Bash
launchctl enable "gui/$(id -u)/net.pangolin.newt"

不要给用户级命令加 sudo,否则容易在错误的用户域里操作。通过 SSH 执行时,目标用户的 GUI 会话也必须存在;没有图形登录会话时,gui/<uid> 域可能不可用。

4. 检查状态和日志

Bash
launchctl list net.pangolin.newt

运行中的任务应能看到 PID。更完整的本地诊断可以使用:

Bash
launchctl print "gui/$(id -u)/net.pangolin.newt"

这份输出可能含环境变量和凭据,只在本机查看。state = running 只证明进程活着,不能代替 Pangolin 中的站点在线状态和外部资源访问测试。

查看日志:

Bash
tail -n 50 "$HOME/Library/Logs/Newt/stdout.log" \
"$HOME/Library/Logs/Newt/stderr.log"

再次按第五节的方法验证外部访问,再注销并重新登录一次,确认任务确实自动启动。测试 Web 服务如果也在注销时退出,需要先重新启动它,不能把应用未启动误判成 Newt 故障。

七、Mac 端常用命令

重启 Newt

Bash
launchctl kickstart -k "gui/$(id -u)/net.pangolin.newt"

-k 会终止当前任务进程并重新启动,会造成短暂断连。如果任务还没加载,先执行 bootstrap。

暂时停止

Bash
launchctl bootout "gui/$(id -u)" \
"$HOME/Library/LaunchAgents/net.pangolin.newt.plist"

它卸载当前任务,不删除配置,下次登录仍可能自动加载。不要用 kill 代替停用:KeepAlive 会把进程拉起来。

修改配置或更换凭据

先 bootout,再在本机编辑 plist 中的对应字段,随后验证并重新加载:

Bash
plutil -lint "$HOME/Library/LaunchAgents/net.pangolin.newt.plist"
launchctl bootstrap "gui/$(id -u)" \
"$HOME/Library/LaunchAgents/net.pangolin.newt.plist"

单纯修改文件或执行 kickstart,不能保证 launchd 重新读取已加载任务的全部配置。轮换凭据时,以服务端新生成的整组参数为准。

禁用自动启动,但保留配置

先执行:

Bash
launchctl disable "gui/$(id -u)/net.pangolin.newt"

如果任务当前已加载,再执行前面的 bootout。恢复时先 enable,再 bootstrap。

彻底移除任务

先按前面的步骤卸载任务,确认不再需要此配置,再删除 plist:

Bash
rm "$HOME/Library/LaunchAgents/net.pangolin.newt.plist"

这不会删除 Newt 二进制、日志或 Pangolin 服务端站点。若不再使用该 Mac,还应在服务端撤销凭据,并处理关联资源。

管理日志大小

这份 launchd 配置不会自动轮转日志。定期检查:

Bash
du -h "$HOME/Library/Logs/Newt/"

需要手工归档时,先 bootout,将日志移出当前文件名,再 bootstrap。下面会保留归档文件,不会清空日志:

Bash
TS=$(date +%Y%m%dT%H%M%S)
for FILE in stdout stderr; do
LOG="$HOME/Library/Logs/Newt/$FILE.log"
if [ -f "$LOG" ]; then
mv "$LOG" "$LOG.$TS"
fi
done

归档仍会占用磁盘,确认保留需求后再清理。不要在没有轮转机制的情况下长期开启 debug 日志。

休眠与真正的长期运行

launchd 负责进程生命周期,不负责保持机器联网。接通电源时,可以在系统设置中启用「显示器关闭时防止自动睡眠」对应选项,具体位置随 macOS 和机型变化;显示器可以熄灭,但系统不能休眠。

笔记本合盖通常仍会休眠。短时间验证可在插电时运行:

Bash
caffeinate -s

这个命令只在运行期间、接通电源时阻止相应系统睡眠,不解决注销、断网、重启解锁或通常的合盖限制。不要为了保持 Newt 在线而随意关闭 FileVault。

最后还要让被代理的应用自身长期运行。Newt 在线、Python 测试服务已经退出时,公网依然会报错;生产应用应使用自己的服务管理方式,不能依赖第五节的临时终端。

八、服务端常用命令与维护

以下命令回到 VPS 执行:

Bash
cd /home/docker/pangolin
# 查看容器状态。
docker compose ps
# 查看最近日志。
docker compose logs --tail=100 pangolin gerbil traefik
# 持续观察日志,Ctrl+C 只退出日志跟踪。
docker compose logs -f --tail=50 pangolin gerbil traefik

按修改类型应用配置

修改内容处理方式
config/config.yml 中的一般配置docker compose restart pangolin;涉及密钥轮换时另走官方流程
Traefik 静态配置docker compose restart traefik
动态路由文件file provider 监视并重新加载;仍需检查日志和实际访问
Compose 镜像版本、挂载或环境docker compose up -d,不能只执行 restart
Gerbil 端口或共享网络配置一起重建 Gerbil 和 Traefik,避免网络命名空间引用不一致

最后一种情况会短暂中断入口和隧道,应在维护窗口执行:

Bash
docker compose up -d --force-recreate gerbil traefik

本例不开放 raw 端口。将来确需添加 TCP/UDP 资源时,必须同时考虑 Pangolin 的允许开关、Gerbil 端口映射、Traefik 的对应 entryPoint、云安全组和认证边界,不能只在面板里填一个端口。

证书续期

自备证书意味着续期由部署者负责。ACME 续期工具应将新的完整证书链和私钥安装到本文约定路径,保持权限,并通过部署钩子触发加载。

不要假定仅替换证书文件就一定触发 Traefik 重载。 可以触发动态配置文件更新,或在维护窗口明确执行:

Bash
docker compose restart traefik

之后从外部重新检查证书的域名、证书链和到期时间。续期成功但部署钩子没有执行,同样会导致线上证书过期。

备份与升级

对这套单机 Compose,最容易复现的一致性备份方式是在维护窗口停止全部服务后,备份整个 config/ 和 Compose 文件。期间所有 Pangolin 入口会中断:

Bash
/bin/bash <<'BASH'
set -euo pipefail
cd /home/docker/pangolin
umask 077
TS=$(date +%Y%m%dT%H%M%S)
docker compose stop
trap 'docker compose start' EXIT
tar czf "../pangolin-backup-$TS.tar.gz" docker-compose.yml config
tar tzf "../pangolin-backup-$TS.tar.gz" >/dev/null
BASH

停止服务成功后,脚本会在正常结束或备份报错退出时尝试恢复服务;如果停止服务本身失败,先检查各容器状态并恢复服务,不要继续备份或升级。执行后检查 docker compose ps,确认服务确实恢复。强制断电等情况不受脚本清理逻辑保障。

确认归档存在、可读取且命令没有报错,再把备份放到安全的异机位置。归档包含数据库、服务器密钥、证书私钥等敏感信息,不能上传公开仓库。

不要把运行中的 SQLite 主文件直接 cp 当作通用一致性备份;在线备份应使用 SQLite backup API,并协调其他配置与密钥的备份。

升级前阅读发布说明,修改需要升级的固定版本,再执行:

Bash
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=100 pangolin gerbil traefik

Pangolin 的迁移可能调整 Traefik 配置中的 Badger 版本,升级后应核对实际文件和仓库模板。数据库已经迁移时,不要假设换回旧镜像就完成回滚;应使用与旧版本配套的一致性备份。

九、按层排错,不要一上来重装

现象先查什么下一步
newt: command not found程序是否在用户目录用 $HOME/.local/bin/newt 的完整路径检查版本
面板 HTTPS 也打不开DNS、证书、80/443 占用、安全组从 VPS 容器日志和外部 DNS 开始,不先改 Mac
Newt 提示认证失败Endpoint、ID、Secret 是否匹配检查站点凭据;丢失时从 Credentials 轮换,不填写哈希
面板可用但站点不在线Newt 日志、UDP 出站、安全组、GerbilHTTPS 通不代表 WireGuard 通
站点在线,但资源返回 502Mac 上的本地 curl 和目标地址确认应用仍在运行、协议端口正确、目标启用
未登录访问返回 401 或跳转资源认证和请求客户端先用已授权浏览器验证,不直接关闭认证
bootstrap 报错plist 格式、是否已经加载、GUI 用户域plutil -lint、launchctl list,必要时先卸载再加载
launchd 进程反复退出可执行文件路径、权限、配置和日志修复原因;不要把缩短重启间隔当成解决方案
注销、合盖后不可用LaunchAgent 和睡眠边界区分任务被卸载、机器休眠和被代理应用退出
修改 plist 后还是旧行为launchd 内存中的已加载配置bootout 后重新 bootstrap

502 的响应时间可以提供线索,但不能单凭「快」就断言防火墙和隧道一定正常。最终要靠目标可达性、日志和实际配置确认。

十、完成后的验收清单

  • 管理域名与资源域名都解析到 VPS,HTTPS 证书可信。
  • Mac 没有路由器入站端口转发,Newt 能主动连接,站点在线。
  • 未授权访问受到保护,授权后能看到 Mac 的测试页面。
  • Newt 由 launchd 管理,关闭终端后继续运行,重新登录后自动启动。
  • 已单独处理应用自启动、系统休眠、日志保留和证书续期。
  • 凭据和私钥没有进入文章或 Git,服务端数据有可恢复的备份。

到这里,公网入口、加密隧道、资源权限和 Mac 进程管理才算连成了一条完整链路。对我来说,重装系统后的恢复也因此变得明确:保留原站点,安全轮换凭据,恢复 Newt 的 launchd 配置,再逐层验证服务,而不是把整套 Pangolin 重新装一遍。

参考资料