文档中心
内网穿透部署与迁移手册
文中所有配置均来自实际运行环境的生效版本。多隧道并行、端口映射、Caddy 路由均已实测。
一、原理与链路
SSH 客户端主动连接服务器并建立隧道,所以本机不需要公网 IP,也不需要在路由器上开放端口。服务器只做转发,不存储数据。
为什么选 SSH 而不是 frp
frp 是专门做内网穿透的工具,功能更强。但在实际网络环境下不可用——本机出口仅放行 22 端口,实测 443 / 8443 / 7000 均在握手后被切断(现象为精确 5 秒后断开,服务器侧无 TIME_WAIT 记录)。
SSH 是唯一稳定通道,且无需额外常驻进程。frp 仍保留在服务器上作为备用,若将来网络环境允许可直接启用。
两个方向的角色
| 角色 | 是什么 | 跑在哪 |
|---|---|---|
| Caddy | 反向代理服务器,接管对外请求并转发 | 云服务器 |
| SSH 客户端 | 建立到服务器的隧道,把本地端口映射过去 | 本机 |
二、当前线上状态
| 项目 | 值 |
|---|---|
| 服务器 | <服务器IP>(腾讯云上海) |
| Caddy HTTP 入口 | 80 端口(主用) |
| Caddy HTTPS 入口 | 30327 端口(备用,兼控制台) |
| 隧道端口 | 8080 → 本机 3002 |
| 访问路径 | /demo1/ |
| 实测延迟 | 约 50–70 ms |
服务器端配置位置
| 文件 | 内容 |
|---|---|
/etc/caddy/Caddyfile | HTTP 80 端口路由(主用) |
/etc/caddy/acli.d/sites/hermes-webui.caddy | HTTPS 30327 路由 + 控制台 |
/etc/caddy/Caddyfile.bak-* | 改动前的备份 |
端口可达性实测
| 端口 | 状态 | 说明 |
|---|---|---|
| 22 | 通 | SSH,隧道依赖此端口 |
| 80 | 通 | HTTP 演示入口,约 53 ms 返回 200 |
| 30327 | 通 | HTTPS 备用入口 |
| 443 / 8443 / 7000 | 被切断 | 握手后 5 秒断开 |
实测过程中 80 端口曾一度不可用、后又恢复。排查时若发现异常,建议先换网络环境(手机热点)复测,区分是配置问题还是出口限制。
三、加一个新项目
以新增一个跑在本地 8080 的项目为例。
端口分配约定
| 本地服务 | 服务器侧端口 | 访问路径 |
|---|---|---|
| 3002(现有项目) | 8080 | /demo1/ |
| 8080(新增) | 12080 | /demo2/ |
| 9000 | 13000 | /demo3/ |
| 4000 | 14000 | /api/ |
新端口需避开已占用项:80、443、30327、8080、8081。
开隧道
# -f 让隧道在后台运行
ssh -N -f \
-o ExitOnForwardFailure=yes \
-o ServerAliveInterval=30 \
-R 12080:127.0.0.1:8080 \
ubuntu@<服务器IP>
确认服务器端已监听:
ssh ubuntu@<服务器IP> "ss -tln | grep 12080"
备份并编辑 Caddy 配置
ssh ubuntu@<服务器IP>
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.bak-$(date +%H%M)
sudo nano /etc/caddy/Caddyfile
加入路由
在 http://<服务器IP> { 块内、结束括号之前加入:
handle /demo2 {
rewrite * /demo2/
reverse_proxy 127.0.0.1:12080 {
header_up Host 127.0.0.1:8080
header_up Origin http://127.0.0.1:8080
}
}
handle_path /demo2/* {
reverse_proxy 127.0.0.1:12080 {
header_up Host 127.0.0.1:8080
header_up Origin http://127.0.0.1:8080
}
}
校验并应用
sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
sudo systemctl restart caddy
curl -s -o /dev/null -w '%{http_code}\n' http://<服务器IP>/demo2/
在 sites/ 目录新建 http://<服务器IP> 站点会报 ambiguous site definition 并导致 Caddy 启动失败。主 Caddyfile 已有同地址块,路由必须写进已有块内。
一键脚本
# 单个隧道
./start-demo.sh 3002
# 并行开多个
./start-demo.sh 3002 8080
加新项目只需改脚本顶部映射表:
| 本地端口 | 服务器端口 | URL 前缀 |
|---|---|---|
| 3002 | 8080 | /demo1/ |
| 8080 | 12080 | /demo2/ |
| 9000 | 13000 | /demo3/ |
| 4000 | 14000 | /api/ |
| 其他 | 本地 + 10000 | /demo<端口>/ |
四、前端项目写法
HTML 返回 200,但 JS 没加载成功,前端框架渲染不出任何内容。
原因
CRA / Vite 这类项目打包后,HTML 里资源引用是绝对路径:
<script defer src="/static/js/bundle.js"></script>
浏览器拿到 /demo2/ 的 HTML 后,会直接去请求
http://<服务器IP>/static/js/bundle.js —— 不带 /demo2 前缀。
如果 Caddy 只配了 /demo2/*,这些请求全部 404。
解法:额外转发根路径资源
handle /static/* {
reverse_proxy 127.0.0.1:8080 {
header_up Host 127.0.0.1:3002
header_up Origin http://127.0.0.1:3002
}
}
handle /runtime-env.js {
reverse_proxy 127.0.0.1:8080 { ... }
}
handle /Inter.ttf {
reverse_proxy 127.0.0.1:8080 { ... }
}
handle /*/logo.ico {
reverse_proxy 127.0.0.1:8080 { ... }
}
若项目新增根路径资源(如 manifest.json、logo192.png),必须再补一条 handle 规则,否则又会白屏。
根本解法
在项目里设 PUBLIC_URL,让构建产物自带路径前缀:
# .env 或 .env.production
PUBLIC_URL=/demo2
改完需重启 dev server。这样资源路径自带前缀,路由就能干净区分,也不必再维护根路径规则。当前方案未改动项目配置。
五、后端服务写法
不涉及静态资源,只需入口路由:
handle /api/* {
reverse_proxy 127.0.0.1:12080 {
header_up Host 127.0.0.1:8080
}
}
若服务自身会重定向,需额外处理路径:
handle /demo2/* {
rewrite * /
reverse_proxy 127.0.0.1:12080 {
header_up Host 127.0.0.1:8080
}
}
| 前端项目 | 后端服务 | |
|---|---|---|
| 静态资源 | 需要处理根路径 | 无 |
| 常见空白页 | 会 | 不会 |
| 推荐做法 | 补根路径规则或设 PUBLIC_URL | 只配入口即可 |
六、多项目共存
/static/* 是根路径,Caddy 按配置顺序匹配、第一条命中即生效。两个前端项目会互相抢静态资源,页面必然错乱。这是已实测确认的行为。
方式 1:分配不同端口与前缀
# 项目 A
handle /a { rewrite * /a/ reverse_proxy 127.0.0.1:8080 { ... } }
handle_path /a/* { reverse_proxy 127.0.0.1:8080 { ... } }
handle /a/static/* { reverse_proxy 127.0.0.1:8080 { ... } }
# 项目 B
handle /b { rewrite * /b/ reverse_proxy 127.0.0.1:12080 { ... } }
handle_path /b/* { reverse_proxy 127.0.0.1:12080 { ... } }
handle /b/static/* { reverse_proxy 127.0.0.1:12080 { ... } }
但这仍要求项目资源路径自带前缀(即设了 PUBLIC_URL=/a/),否则浏览器仍请求 /static/js/bundle.js,匹配不到 /a/static/*,又会白屏。
方式 2:改项目 PUBLIC_URL(根本解法)
在各项目 .env 中加 PUBLIC_URL=/a,重启 dev server。构建产物自带前缀后,路由可干净区分。
方式 3:分开部署
不同项目用不同端口甚至不同服务器,简单但资源浪费。
七、迁移到新服务器
新服务器只需装 Caddy
sudo apt update && sudo apt install -y caddy
其余组件均不需要,本方案不依赖 frp。
检查端口是否放行
这一步最容易踩坑。云服务器需在控制台安全组放行端口,否则连接会在握手后被切断。
| 端口 | 用途 | 优先级 |
|---|---|---|
| 22 | SSH | 必需 |
| 80 | HTTP 演示入口 | 必需 |
| 443 | HTTPS | 可选 |
| 高位端口 | frp 备用方案 | 可选 |
验证时必须从外部测,服务器内部自测无效:
curl -s -o /dev/null -w 'code=%{http_code} time=%{time_total}\n' \
--max-time 10 http://<新服务器IP>/
识别被掐断的特征
若 time 恰好为 5.000 左右且 code=000,说明连接被中间设备按协议识别并切断,不是配置问题。这种情况应换网络环境或联系网络管理员。
迁移步骤
推送本机公钥
ssh-copy-id ubuntu@<新服务器IP>
备份旧配置留档
ssh ubuntu@<旧服务器IP> "sudo cat /etc/caddy/Caddyfile" > caddy-backup.txt
写入新配置
新服务器无托管配置约束,Caddyfile 可更简洁:
{
admin off
}
http://<新服务器IP> {
handle /demo1 {
rewrite * /demo1/
reverse_proxy 127.0.0.1:8080 {
header_up Host 127.0.0.1:3002
header_up Origin http://127.0.0.1:3002
}
}
handle_path /demo1/* {
reverse_proxy 127.0.0.1:8080 {
header_up Host 127.0.0.1:3002
header_up Origin http://127.0.0.1:3002
}
}
handle /static/* {
reverse_proxy 127.0.0.1:8080 {
header_up Host 127.0.0.1:3002
header_up Origin http://127.0.0.1:3002
}
}
handle /runtime-env.js {
reverse_proxy 127.0.0.1:8080 { ... }
}
handle /Inter.ttf {
reverse_proxy 127.0.0.1:8080 { ... }
}
handle /*/logo.ico {
reverse_proxy 127.0.0.1:8080 { ... }
}
}
启动并设为开机自启
sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
sudo systemctl enable caddy && sudo systemctl restart caddy
修改本机脚本中的地址
或直接编辑脚本顶部的 SERVER 变量:
SERVER="${TUNNEL_SERVER:-ubuntu@<新服务器IP>}"
重启隧道
./stop-demo.sh
./start-demo.sh 3002
迁移验收清单
- 新服务器安全组已放行 22 和 80
- 从外部测
curl http://<新IP>/返回 200(非 5 秒超时) - Caddy 已
enable,重启后仍生效 - 本机公钥已推送到新服务器
- 脚本中的地址已全部替换
- 隧道建立后
ss -tln | grep 8080有输出 - 页面返回 200 且
bundle.js能加载(不是空白页)
八、故障排查
| 现象 | 原因 | 处理 |
|---|---|---|
| 完全打不开,5 秒超时 | 网络出口或安全组拦截 | 换网络环境测;检查安全组;nc -z <IP> 80 |
code=000 且耗时正好 5.000 秒 | 被中间设备按协议识别切断 | 非配置问题,换网络或换端口 |
| 页面空白 | 静态资源 404 | 补 /static/* 等根路径规则;强制刷新缓存 |
| 自动跳 https | 用了 30327 端口 | 改用 80 端口的 http 地址 |
| 502 / 504 | 隧道断了但 Caddy 还挂着 | 重跑 start-demo.sh |
ambiguous site definition | 重复定义同地址站点 | 路由写进已有块,不新建同地址站点文件 |
| Caddy reload 失败 | 设了 admin off,管理端口关闭 | 用 restart 不用 reload |
| 多前端项目资源错乱 | /static/* 根路径冲突 | 改 PUBLIC_URL,或分开部署 |
诊断命令
# 本机:隧道是否在
pgrep -f "ssh -N.*-R 8080"
# 服务器:隧道是否在
ssh ubuntu@<IP> "ss -tln | grep 8080"
# 服务器:端到端(必须带 Host 头,否则会跳 https)
ssh ubuntu@<IP> "curl -s -o /dev/null -w '%{http_code}\n' \
-H 'Host: <IP>' http://127.0.0.1/demo1/"
# Caddy 日志
ssh ubuntu@<IP> "sudo journalctl -u caddy --no-pager -n 30"
九、命令速查
隧道管理
./start-demo.sh 3002 # 启动(参数为本地端口)
./start-demo.sh 3002 8080 # 并行开两个
./stop-demo.sh # 停止全部
cat /tmp/tunnel.log # 隧道日志
服务器运维
ssh ubuntu@<IP>
sudo systemctl status caddy
sudo systemctl restart caddy
sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
sudo tail -50 /var/log/caddy.log
free -h # 内存占用参考
改配置前必做
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.bak-$(date +%H%M)
十、注意事项
当前服务器的 Caddyfile 设置了 admin off,管理端口 2019 关闭,reload 必定失败。
当前这台的主配置文件由外部程序托管,误改会导致 Caddy 起不来。新服务器虽为自主管理,仍建议保持备份习惯。
不要安装数据库、编译环境或本地大模型。当前 Caddy 与控制台服务共占约 30 MB,负载很轻。
HTTPS 入口走 ACME 短期证书 profile,续期依赖服务器能访问证书颁发机构。若网络受限导致续签失败,需手动介入。