系统正常 友情链接
KNOWLEDGE BASE • DOCUMENTATION

文档中心

共 1 份手册 所有地址已脱敏
按主题查阅操作手册与说明文档 快速定位部署、迁移与故障排查所需的信息。
更新持续同步
← 返回手册列表

内网穿透部署与迁移手册

SSH 反向隧道 Caddy 反向代理 投标演示 / 客户查看 地址已脱敏
已验证可用

文中所有配置均来自实际运行环境的生效版本。多隧道并行、端口映射、Caddy 路由均已实测。

一、原理与链路

客户浏览器
外部访问者
↓
http://<服务器IP>/<前缀>/
云服务器 · Caddy
80 端口 · reverse_proxy
↓
127.0.0.1:<服务器侧端口>
SSH 反向隧道
由本机主动向外建立的长连接
↓
回到本机
本地开发服务
127.0.0.1:<本地端口>
关键:连接由内向外建立

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/CaddyfileHTTP 80 端口路由(主用)
/etc/caddy/acli.d/sites/hermes-webui.caddyHTTPS 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/
900013000/demo3/
400014000/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 前缀
30028080/demo1/
808012080/demo2/
900013000/demo3/
400014000/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。

检查端口是否放行

这一步最容易踩坑。云服务器需在控制台安全组放行端口,否则连接会在握手后被切断。

端口用途优先级
22SSH必需
80HTTP 演示入口必需
443HTTPS可选
高位端口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)

十、注意事项

Caddy 改配置用 restart,不要用 reload

当前服务器的 Caddyfile 设置了 admin off,管理端口 2019 关闭,reload 必定失败。

改主配置前先备份

当前这台的主配置文件由外部程序托管,误改会导致 Caddy 起不来。新服务器虽为自主管理,仍建议保持备份习惯。

服务器内存仅 1.9 GB

不要安装数据库、编译环境或本地大模型。当前 Caddy 与控制台服务共占约 30 MB,负载很轻。

证书自动续期

HTTPS 入口走 ACME 短期证书 profile,续期依赖服务器能访问证书颁发机构。若网络受限导致续签失败,需手动介入。