来源:intranet-tunnel/deploy/edge/README.md(整篇)(原文 6509 字符)

边缘配置自动同步

让「在面板配好一条隧道」到「域名可以访问」之间不再有人工步骤。


它解决什么

隧道域名最终是由服务端内置反向代理按 Host 路由的,但公网 443 上的 TLS 必须由前置 nginx 终止。于是每新增一个对外域名,nginx 里就要有对应的 server 块,否则请求根本到不了服务端。

手工维护这件事有三个反复出现的问题:忘改、改错证书路径、改完忘 reload。 本模块把它变成自动的。

核心设计:泛域复用

域名类型边缘配置新增隧道要做什么
落在 EDGE_BASE_DOMAINS 泛域内与同泛域其它域名共用一段通配 server什么都不用做
泛域之外(如 erp.company.com自动生成专属 server 块 + 用该域名自己的证书先为它签发/导入证书

一百条泛域隧道和一条的配置文件一样大。 这不是省事,而是让配置规模 与隧道数量解耦——后者会持续增长,前者不该跟着长。

三种部署形态,同一套渲染逻辑

服务端只负责写文件,不直接令 nginx 生效。这不是偷懒,是硬约束:

  • 宝塔场景下 nginx 在宿主机,容器内看不到它的进程,发不了 HUP 信号;
  • 唯一替代是挂 docker.sock 让服务端去 docker exec,那等于把宿主机

root 权限交出去。

于是约定变成「文件变了就 reload」,把 reload 交给外部监听者。 两种形态的差异因此被压缩到只剩配置值:

纯 Docker(compose nginx)宝塔(宿主机 nginx)
EDGE_OUTPUT_DIR./edge/conf.d(挂给两侧)挂载宝塔的 vhost 目录
EDGE_UPSTREAM_DATAhttp://tunnel-server:48080http://127.0.0.1:48080
EDGE_CERT_PATH_PREFIX/etc/nginx/certs/home/docker/<项目>/certs
reload 触发deploy/edge/docker/edge-entrypoint.sh(容器内看门狗)deploy/edge/systemd/(path unit)
EDGE_VALIDATE_CMD / EDGE_RELOAD_CMD留空(由看门狗做)留空(由 path unit 做)

第三种形态是不用 nginx:保持 EDGE_SYNC_ENABLED=false, 服务端自己在 48443 上终止 TLS。适合内网或仅测试的场景。


形态一:纯 Docker

docker-compose.yml 里相关片段(已配好):

tunnel-server:
  volumes:
    - ./edge/conf.d:/app/edge/conf.d

nginx:
  entrypoint: ["/edge/edge-entrypoint.sh"]
  command: ["nginx", "-g", "daemon off;"]
  volumes:
    # 注意不是只读:看门狗要在校验失败时把坏配置移出这个目录。
    - ./edge/conf.d:/etc/nginx/edge.d
    - ./edge/rejected:/etc/nginx/rejected
    - ./deploy/edge/docker/edge-entrypoint.sh:/edge/edge-entrypoint.sh:ro

nginx-compose.conf 已经 include /etc/nginx/conf.d/*.conf,无需改动。

.env

EDGE_SYNC_ENABLED=true
EDGE_OUTPUT_DIR=/app/edge/conf.d
# 注意这是「服务端容器内」的路径;nginx 侧看到的是 /etc/nginx/conf.d
EDGE_CERT_PATH_PREFIX=/etc/nginx/certs
EDGE_UPSTREAM_DATA=http://tunnel-server:48080
EDGE_UPSTREAM_API=http://tunnel-server:47801
EDGE_BASE_DOMAINS=*.tunnel.example.com
EDGE_MANAGE_HTTP=true      # compose 场景下由本模块接管 80 端口

形态二:宝塔(或任何宿主机 nginx)

服务端容器里的 /app/edge/conf.d 必须挂到宿主机的目录,且该目录要被 nginx 的 include 覆盖

tunnel-server:
  volumes:
    - ./edge/conf.d:/app/edge/conf.d

宝塔的站点配置文件(如 /www/server/panel/vhost/nginx/tunnel-edge.conf):

include /home/docker/intranet-tunnel/edge/conf.d/*.conf;

⚠️ 挂载进来的目录属主是 root,而服务端容器以 uid 10001 运行。 首次部署前执行:

mkdir -p /home/docker/intranet-tunnel/edge/conf.d
chown -R 10001:10001 /home/docker/intranet-tunnel/edge

安装重载触发器:

install -m 755 deploy/edge/systemd/tunnel-edge-reload.sh /usr/local/bin/
install -m 644 deploy/edge/systemd/tunnel-edge-reload.service /etc/systemd/system/
install -m 644 deploy/edge/systemd/tunnel-edge-reload.path    /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now tunnel-edge-reload.path

.env

EDGE_SYNC_ENABLED=true
EDGE_OUTPUT_DIR=/app/edge/conf.d
EDGE_CERT_PATH_PREFIX=/home/docker/intranet-tunnel/certs
EDGE_UPSTREAM_DATA=http://127.0.0.1:48080
EDGE_UPSTREAM_API=http://127.0.0.1:47801
EDGE_BASE_DOMAINS=*.tunnel.example.com,*.t.example.com
# 这台机器 80/443 的 default_server 已由宝塔占用,必须保持 false,
# 否则 nginx -t 会报 duplicate default server。
EDGE_MANAGE_HTTP=false
EDGE_MANAGE_DEFAULT=false
# 宝塔的 nginx 通常够新,支持 http2 on; 语法。
EDGE_HTTP2=true
# 保留宝塔的站点流量统计(否则接管隧道域名会顺手关掉它)。
EDGE_EXTRA_INCLUDE=/www/server/panel/vhost/nginx/extension/<站点>/*.conf

接管范围:只接管 443

在宝塔这类环境里,不要把手工的站点文件整个删掉——它通常还兼着别的职责:

段落谁管为什么
80 端口跳转宝塔(保持 EDGE_MANAGE_HTTP=false宝塔已有 80 的 default_server,重复声明会让 nginx -t 失败
443 的 server本模块这正是要自动化的部分
站点流量统计 include宝塔(用 EDGE_EXTRA_INCLUDE 带进新配置)不带上就等于把统计关掉了,而用户会以为是统计坏了

所以改造后,手工站点文件长这样:

# 80 端口块原样保留
server {
    listen      80;
    server_name *.tunnel.example.com;
    location / { return 301 https://$host$request_uri; }
}

# 443 的 server 块由 intranet-tunnel 自动生成并维护
include /home/docker/intranet-tunnel/edge/conf.d/*.conf;

⚠️ 只能有一个文件 include 这个目录。两个泛域站点各 include 一次, 同一个 server_name 就会出现两次,nginx 启动时会告警并按第一个匹配, 表现为「改了配置但行为没变」。 因此第二个泛域站点里的 443 块要删掉(只留 80 块与注释)。

⚠️ 别在宝塔面板里"保存"这些改过的站点文件——面板会用它的模板重写, 把 include 覆盖掉。要改就改本模块的 EDGE_* 配置。


两个必须理解的坑

1. 证书路径是「nginx 看到的」,不是「服务端看到的」

同一份证书文件,在两边路径不同:

服务端容器:  /app/certs/tunnel.example.com/cert.pem
nginx 容器:  /etc/nginx/certs/tunnel.example.com/cert.pem
宝塔宿主机:  /home/docker/intranet-tunnel/certs/tunnel.example.com/cert.pem

EDGE_CERT_PATH_PREFIX 填的是中间那个或第三个。填错的表现是 nginx -tcannot load certificate ... No such file or directory, 而面板上证书显示一切正常。

2. HTTP/2 的语法在 nginx 1.25.1 变了

# nginx < 1.25.1
listen 443 ssl http2;
# nginx >= 1.25.1
listen 443 ssl;
http2 on;

两者互不兼容,写死任意一种都会让另一类环境 nginx -t 直接失败。 所以由 EDGE_HTTP2 决定,默认 false(不带 http2 指令,两边都能跑)。


校验失败会做什么

边缘侧在 reload 之前总是先跑一次校验(systemd 脚本与容器看门狗都是如此), 校验不通过时做两件事:

  1. 放弃 reload —— nginx 继续用内存里的旧配置服务,影响面仅限本次改动;
  2. 把坏配置移出 include 目录(移到 edge/rejected/,带时间戳)。

第 2 步容易被忽略,但同样重要:坏文件若留在原地,下一次 nginx 启动 (容器重启、宿主机重启、版本升级)会把它加载进来,届时 nginx 直接起不来。 那比「某次改动没生效」严重得多——后者只影响一个域名,前者影响这台 nginx 上的 所有站点,而且故障与本次改动之间隔着一次重启,极难关联。

⚠️ 因此 compose 里 ./edge/conf.d 不能挂成只读:只读挂载下 nginx 容器无法移走坏文件,上面第 2 步就落不了地。它挂的是可写。

被移出的文件保留在 edge/rejected/ 供排查:它的内容就是出问题的那一份, 直接对它跑 nginx -t -c 就能看到具体报错。

服务端那一侧的 EDGE_VALIDATE_CMD 是另一层保护(用于纯 Docker 等 服务端能自己跑校验的场景);宝塔场景下服务端容器内没有可用的 nginx, 校验只能放在边缘侧,两者是互补而非替代关系。

验证

# 1) 看渲染结果(不落盘)——不用登服务器就能确认某个域名有没有被算进来
curl -sk -H "Authorization: Bearer <token>" \
  https://panel.example.com/api/edge/preview | jq -r .content

# 2) 立即同步一次
curl -sk -X POST -H "Authorization: Bearer <token>" \
  https://panel.example.com/api/edge/sync | jq

# 3) 看状态
curl -sk -H "Authorization: Bearer <token>" \
  https://panel.example.com/api/edge/status | jq

关键字段:

字段含义
wildcard_domains / dedicated_domains泛域内 / 泛域外的域名数。前者增长不会让配置变大
changed本次是否真的写了文件(false 说明内容没变,这是幂等生效的证据)
validated / reloaded校验与重载是否执行
warnings哪些域名因缺证书没生成配置,以及为什么
pending_reload内容已落盘但重载失败,下轮对账会重试

宝塔场景看重载日志:

journalctl -u tunnel-edge-reload -n 50

端到端验证:不重启任何东西

最有力的验证是全程不碰服务端

  1. 面板新增一条隧道,域名落在泛域内
  2. 观察服务端日志出现 隧道路由已同步到内置反向代理
  3. 观察边缘日志出现一次重载
  4. 直接访问该域名,应返回 200,且响应头带 X-Proxy-By: intranet-tunnel

第 4 步的响应头是关键证据:它证明请求确实经过了内置反向代理, 而不是被 nginx 的某个默认站点接走了。


来源:intranet-tunnel/deploy/nginx/certs/README.md(整篇)(原文 1255 字符)

TLS 证书目录

把 Nginx 使用的证书放在本目录(容器/宿主机路径为 /etc/nginx/certs/):

文件说明
fullchain.pem服务器证书链(含中间证书),对应 ssl_certificate
privkey.pem服务器私钥,对应 ssl_certificate_key
default.crt兜底自签证书,用于未绑定域名的 443 访问
default.key兜底自签证书私钥

1. 申请通配符证书(Let's Encrypt / certbot)

通配符证书必须使用 DNS-01 挑战:

certbot certonly \
  --manual \
  --preferred-challenges dns \
  -d 'example.com' -d '*.example.com'

生成后把证书软链或复制到本目录:

ln -sf /etc/letsencrypt/live/example.com/fullchain.pem \
       /etc/nginx/certs/fullchain.pem
ln -sf /etc/letsencrypt/live/example.com/privkey.pem \
       /etc/nginx/certs/privkey.pem

2. 自动续期

/etc/cron.d/certbot-renew

0 3 * * * root certbot renew --quiet --deploy-hook "nginx -s reload"

说明:

  • ACME 的 HTTP-01 挑战路径已在 conf.d/tunnel-http.conf 的 80 端口 server 中放行;
  • 使用 DNS-01 时无需放行该路径,但需要 DNS 服务商的 API 凭据。

3. 兜底自签证书

openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
  -keyout default.key -out default.crt -subj "/CN=invalid"

4. 安全提示

  • privkey.pem 权限建议为 600,属主为 root
  • 本目录下的 *.pem / *.key / *.crt 已被仓库 .gitignore 排除,请勿提交私钥;
  • 服务端自身的控制连接 TLS 证书是另一套(由服务端启动时自签或通过

TLS_CERT_FILE / TLS_KEY_FILE 指定),与本目录无关。

技术文档库 / 域名与证书接入 0 0 sushike
2026-09-13T12:41:54.845618703Z 2026-09-13T13:37:18.352921932Z