用 Caddy + SSH 反向隧道 + Let’s Encrypt 给开发服务开一个临时线上预览域名

场景很常见:前端同学的 Next.js 服务还跑在自己本机,后端、产品、老板却急着要看效果。今天记录一次真实操作——把本机服务通过「SSH 反向隧道 + Caddy 反向代理 + 自动 HTTPS」串起来,几分钟内就给一个开发中的页面配上了一个可公网访问的 HTTPS 域名。

问题背景

开发者在本地起了一个 Next.js 项目(监听 3000 端口),但评审需要其他人能在公网访问。服务器上没有这个项目的代码,也不方便直接部署。

于是有了这套「隧道 + 代理」的组合拳:

  • 开发者用 SSH 反向隧道,把服务器的某个端口转发到自己本机的 3000;
  • 服务器上用 Caddy 把某个子域名反代到这个本地端口;
  • Caddy 自动申请并续期 Let’s Encrypt 证书,让这个子域名直接就是 HTTPS。

最终效果:任何人在浏览器打开 https://.,就能看到开发者本机正在跑的那个页面。

第一步:确认隧道通不通

在服务器上,先探一下那条 SSH 隧道对应的本地端口(这里假设是 13000):

curl -s -o /dev/null -w "%{http_code}n" http://127.0.0.1:13000/
curl -s http://127.0.0.1:13000/ | head -20
  • 如果返回 200,且能拿到 HTML(能看到 Next.js 的页面骨架),说明隧道是通的,可以继续;
  • 如果返回 000 或直接报连接错误,说明隧道没建立起来,就别往下走了,先让开发者重建 SSH 隧道。

这一步很重要:先验证后端再改配置,否则你配好了 Caddy,结果反代到一个打不开的空端口,白忙一场。

第二步:找到 Caddy 真正用的配置文件

很多人(包括我)第一反应是去 /etc/caddy/Caddyfile,但不同安装方式路径可能不一样,别想当然。用这几种方式确认:

systemctl cat caddy 2>/dev/null          # 看 systemd 服务定义里的 --config
ps aux | grep -i caddy | grep -v grep    # 看实际运行进程的启动参数
ls -la /etc/caddy/Caddyfile /usr/local/caddy/Caddyfile 2>/dev/null

服务文件里那两行最关键:

ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile

--config 指向的路径,才是真正生效的配置文件。以实际运行进程的参数为准。

第三步:改之前先备份

改配置前一定先备份,出问题能秒回滚:

cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.bak.$(date +%Y%m%d%H%M%S)

带「时间戳 + .bak」后缀,一眼能看出是哪一次改的。

第四步:新增一个 site block

在 Caddyfile 里补一个站点块(如果已经有同名块就先改它,别重复加):

h5-video.example.com {
    reverse_proxy 127.0.0.1:13000
}

几个要点:

  • 不要动其它已有的站点块,只追加你自己这一个;
  • reverse_proxy 的目标固定是那个隧道端口(127.0.0.1:13000),别手滑改成别的端口;
  • 加了新域名后,Caddy 会自动去申请证书。

第五步:重载 Caddy

systemctl reload caddy

如果这个服务不存在,就退回到 caddy reload --config 。reload 是非中断的平滑重载,已经跑着的站点不会断。

第六步:验证与收尾

# 1) 确认隧道仍然通
curl -s -o /dev/null -w "%{http_code}n" http://127.0.0.1:13000/

# 2) 本地强制解析域名到本机,测试 HTTPS(DNS 还没指过来时也能测)
curl -sI --resolve h5-video.example.com:443:127.0.0.1 https://h5-video.example.com/

一个很关键的坑:DNS 和证书申请的顺序

这次操作里最典型的坑,就是证书申请时机

第一次 reload 之后,我在 Caddy 的日志里看到 ACME 挑战一直失败,报错关键词是:

DNS problem: NXDOMAIN looking up A for h5-video.example.com

原因是:当时 DNS 记录还没指过来,Let’s Encrypt 要校验域名(HTTP-01 或 TLS-ALPN-01 挑战),它去查这个域名的 A 记录,查到的是 NXDOMAIN(域名不存在),校验自然失败。

所以正确的顺序应该是:

  • 先把 DNS 的 A 记录指到服务器 IP
  • 等解析生效(可以用公共 DNS 确认,比如阿里云 223.5.5.5、DNSPod doh.pub,或者直接 getent hosts 看本地解析);
  • systemctl reload caddy,让 Caddy 去申请证书。

DNS 一旦生效,reload 之后再看日志,就能看到证书成功签发的字眼:

tls.obtain  certificate obtained successfully
authorization finalized  authz_status: valid
tls  served key authentication certificate  challenge: tls-alpn-01

再验证,就能看到 HTTP/2 200,且响应头里带着 server: Caddyx-powered-by: Next.js,整条链路就通了。

小结

这套组合的巧妙之处在于:

  • SSH 反向隧道:免部署,把「本机开发服务」变成「服务器上的一个本地端口」;
  • Caddy 反向代理:一行配置把子域名接到那个端口;
  • 自动 HTTPS:Caddy 内置 ACME,证书申请、续期全自动,零维护。

整个过程唯一容易翻车的地方就是「DNS 没生效就急着让 Caddy 申请证书」。把顺序捋对了,剩下就是几分钟的事。


*本文记录了一次真实的运维小操作,域名和 IP 已做脱敏处理。*