0
0
0

宝塔与 nginx 反代

文章摘要
|

来源:intranet-tunnel/deploy/cloud/BAOTA-DEPLOY.md(整篇)(原文 8031 字符)

宝塔共存部署记录(2026-09-12 实测)

本文记录把 intranet-tunnel 部署到已运行宝塔面板的云服务器的完整过程与全部踩坑。 通用步骤见同目录 README.md;本文只讲与它不同的部分


一、目标环境事实

实例i-wz9diqyfq2owhs5dn8gx(sushike,cn-shenzhen)
系统Ubuntu 24.04.4 LTS / x86_64 / 2 vCPU / 1.5 GiB
公网 / 内网<公网IP> / <内网IP>
已占端口宝塔 nginx:80/443/888/8443/40376;已有容器:9898、3000、5432、8080、8081
可用内存约 500 MB(部署后仍足以运行 3 个容器)
部署工具Workbench CLI(/home/docker/intranet-tunnel

二、四个必须绕开的冲突

1. 80/443 被宝塔占用 —— 不启 compose 里的 nginx 容器

宝塔的 nginx 正在服务 www.sushike.cloudbarbershop.sushike.cloud, 再起一个容器抢 80/443 会直接冲突。

做法:只启三个服务,把 80/443 留给宝塔 nginx 作前置。

docker compose up -d tunnel-server postgres db-backup    # 注意:不含 nginx

nginx 配置位于 baota/tunnel.sushike.cloud.confbaota/wildcard-t.sushike.cloud.conf, 安装到 /www/server/panel/vhost/nginx/(主配置第 96 行已 include 该目录)。

回归验证要点barbershop.sushike.cloud.conf:140listen 443 ssl default_server, 所以新增的 443 块不会劫持未匹配的 Host;www.sushike.cloud.conf 只有 listen 80, 它没有 HTTPS 属既有状况。

2. DNS:* 泛解析被占用,且 _acme-challenge 被 ESA 委派

*                     CNAME  all.sushike.cloud.a1.initaf.com   ← 在用,不能动
_acme-challenge       CNAME  sushike.cloud.<...>.dcv.aliyun-esa.com
  • 抢占 * 会切断 initaf 的现有服务;
  • _acme-challenge 被委派成 CNAME 后,CNAME 与 TXT 不能共存

sushike.cloud 申请泛域名证书的 DNS-01 必然失败。

做法:隧道改用独立的二级泛域名 *.t.sushike.cloud,其验证名 _acme-<服务名>.t.sushike.cloud 与现有记录互不冲突。

新增两条解析(均为新增,未改动任何现有记录):

记录类型
tunnelA<公网IP>
*.tA<公网IP>

3. 内存:原 .env 的资源上限超过物理内存

.env 默认 SERVER_MEM_LIMIT=2gDB_MEM_LIMIT=2g,而整机只有 1.5 GiB。 按实测下调(该机 available 约 500 MB):

变量原值实际值
SERVER_CPU_LIMIT / SERVER_MEM_LIMIT / SERVER_MEM_RESERVE2.0 / 2g / 256m1.0 / 512m / 64m
DB_CPU_LIMIT / DB_MEM_LIMIT / DB_MEM_RESERVE2.0 / 2g / 256m1.0 / 320m / 96m

4. 面板默认禁止外网访问,且该开关以数据库为准

.envPANEL_OUTSIDE_ACCESS 只在 system_settings 表首次初始化时写入, 运行期读的是表里的 panel.outside_access。于是形成鸡生蛋: 要改设置得先进面板,要进面板得先改设置。

绕过:直接改数据库(fix-panel-access.sh 已封装)。

update system_settings set setting_value='true' where setting_key='panel.outside_access';

注意表结构是 setting_key / setting_value不是 key / value


三、三个必须修的配置问题

1. certs / backups 从未挂载到宿主机(compose 缺陷)

原 compose 里 tunnel-server 只挂了 tunnel-data:/app/data, 而注释却写着「宿主机 ./certs 即服务端容器的 /app/certs」—— 该映射并不存在server/Dockerfile/app/logs/app/backups/app/certs 声明了匿名卷,所以证书不会丢,但也不会出现在宿主机 ./certs, 而 nginx 读的正是宿主机 ./certs,结果是空目录。

已在 compose 中补上:

      - ./certs:/app/certs
      - ./backups:/app/backups

2. bind mount 后属主变成 root,服务端起不来

容器以 USER tunneluid 10001)运行。匿名卷会继承镜像内目录的属主, bind mount 不会。不改属主的报错是:

服务端启动失败: cert: 写入 ACME 账户密钥失败: open certs/acme_account.key: permission denied

首次部署必做

mkdir -p certs backups logs && chown -R 10001:10001 certs backups logs

3. /app/logs 同样没映射:文件日志只落在匿名卷里

LOG_FILE_ENABLED=true 时服务端会写 app.log / audit.log, 但 /app/logs 也是 Dockerfile 声明的匿名卷 —— 宿主机 ./logs 是空的, 日志既看不到也无法被采集,排查问题只能靠 docker logs

已在 compose 中补上:

      - ./logs:/app/logs

日志轮转不要另配外部 logrotate:程序用 gopkg.in/natefinch/lumberjack.v2 自带轮转 (LOG_FILE_MAX_SIZE=100 MB / LOG_FILE_MAX_BACKUPS=30 / LOG_FILE_MAX_AGE=30 天)。 外部 logrotate 的 copytruncate 会与 lumberjack 的「改名 + 新建」 并发操作同一个文件而互相打架。

顺带一个 logrotate 语法坑:它的 create 只接受用户名, 写容器内的 UID(如 create 0600 10001 10001)会直接报 unknown user '10001'跳过整条规则 —— 宿主机上并没有这个用户。

⚠️ 磁盘上界:日志分 app / err / audit 三类,各自独立按 MaxSize × MaxBackups 计,当前配置最坏约 9 GB。 若嫌大,改 .envLOG_FILE_MAX_SIZE=20LOG_FILE_MAX_BACKUPS=10 (每类 200 MB)后重建容器即可。


四、证书:面板自带的 DNS-01 有缺陷,改用 acme.sh

缺陷现象

cert: 写入 DNS-01 TXT 记录失败: ddns: 阿里云返回错误 InvalidDomainName.NoExist

根因(server/internal/cert/acme.go:312-315

// 通配符证书的授权标识符是主域名(不带 *),TXT 记录写在其 _acme-challenge 下。
domain := strings.TrimPrefix(authz.Identifier.Value, "*.")
if err := dnsProv.EnsureRecord(ctx, domain, "_acme-challenge", "TXT", record, 60); err != nil {

它把「ACME 授权标识符」直接当作 DNS 托管主域名传给服务商。 对 tunnel.sushike.cloud 而言 domain 就是它本身, 而阿里云云解析要求 DomainName 必须是托管主域 sushike.cloud,于是报域名不存在。 缺的是「待签域名 → DNS 托管主域」的解析(PSL 或用户显式配置)。

规避

用 acme.sh 以 DNS-01(dns_ali)签发,产物落到面板同一证书目录:

ALI_KEY=... ALI_SECRET=... CERT_EMAIL=... sh setup-acme.sh
  • 覆盖 tunnel.sushike.cloud + *.t.sushike.cloud(SAN)
  • 续期由 acme.sh 自带 cron 负责(每天 4/10/16/22 点),

--reloadcmd 已配成自动 nginx -s reload

  • 不要再把该证书导入面板的证书模块:两边同时续期会互相干扰

另注:源码落后于镜像

面板 API 申请证书时会报「DNS-01 验证需要选择 DNS 服务商并填写凭据」, 但仓库源码里 handleCreateCert 并未把 dns_provider / dns_credentials 传给签发器, 且该报错文案在源码中不存在 —— 说明运行中的镜像比仓库源码新, 这是一处待对齐的差异(详见 issue-cert.sh 顶部注释)。


五、端口与防火墙

端口用途安全组ufw
80 / 443面板 + 隧道 HTTPS(宝塔 nginx)已放行已放行
47800控制连接(客户端接入)本次新增本次新增
48081-48100TCP 隧道端口池本次新增本次新增
47801 / 48080管理 API / 数据面只绑 127.0.0.1,不要放行
48443 / 49000-49100内置反代 HTTPS / 端口转发按需放行按需

安全组规则以新增方式追加,未删除任何既有规则。


六、最终状态

tunnel-server    Up (healthy)   127.0.0.1:47801, 0.0.0.0:47800, 0.0.0.0:48081-48100, 127.0.0.1:48080
tunnel-postgres  Up (healthy)
tunnel-db-backup Up
入口结果
https://tunnel.sushike.cloud/200(证书 CN=tunnel.sushike.cloud,含 SAN *.t.sushike.cloud
http://tunnel.sushike.cloud/301 → HTTPS
https://<任意>.t.sushike.cloud/404(尚无隧道规则时属预期)

七、Workbench CLI 的两个传参坑(用本工具部署时必踩)

  1. 远程命令里以 - 开头的 token 会被 workbench 当成自己的 flag。

报错形如 bad flag syntax: ---unknown shorthand flag: 'l' in -l(后者来自 wc -l)。 PowerShell 5.1 把多行字符串传给原生程序时会拆分参数。 对策:把远程脚本写成文件上传,再用 sh /path/script.sh 这种极简命令执行。

  1. upload 有覆盖保护,默认答案为 No,非交互场景会直接终止。

重新上传前先 rm 远程同名文件。

另:exec 的 stdout 会被截断,psql 的对齐表格尤其明显, 需要完整输出时把结果重定向到文件再 cat


八、宝塔网站统计与 nginx 日志(2026-09-12 补充)

最初用手写 conf 建站点,导致两个功能都不可用 —— 而且它们是同源的: 站点不在宝塔的 site.db 里,所以统计采集不到它; 而 ./logs/nginx/ 一直是空的(那是 compose 内 nginx 服务预留的目录,本项目用的是宝塔 nginx)。

1. 统计为什么看不到

宝塔统计不读日志文件site_total 服务(/www/server/site_total/site_total,systemd 常驻) 监听 Unix socket,nginx 经 syslog 协议把访问日志实时推给它:

# /www/server/panel/vhost/nginx/extension/<域名>/site_total.conf
access_log syslog:server=unix:/tmp/site_total.sock,nohostname,tag=<站点ID>__access site_total;

关键在 tag=<站点ID>__access —— 这个编号就是 /www/server/panel/data/db/site.dbsites 表的 id站点不在库里 → 没有 ID → 统计无从归属。

手写 conf 建立的站点必须补三件事:

  1. site.dbsites + domain 表注册站点,拿到 ID
  2. extension/<域名>/site_total.conf,写入带该 ID 的 access_log syslog:...
  3. 在该站点 conf 里 include 这个 extension 目录

本项目站点已注册为 ID=3tunnel.sushike.cloud), 统计产物可在 /www/server/site_total/data/total/tunnel.sushike.cloud/ 下看到。

副作用:宝塔面板的「网站」列表里会多出 tunnel.sushike.cloud,这是统计工作的前提。 改动前的 site.db 已备份为 site.db.bak.<时间戳>

2. nginx 日志的三路输出

同一份访问日志同时写三处(nginx 允许多条 access_log 累加):

去向用途
/www/wwwlogs/tunnel*.log宝塔面板「网站日志」界面读这里
./logs/nginx/*.log落到项目目录,与容器日志统一,便于采集与排查
extension 里的 syslog宝塔网站统计

3. 轮转分工(不要混着来)

对象谁负责方式
/www/wwwlogs/*.log宝塔自己宝塔的日志切割
./logs/nginx/*.log本项目/etc/logrotate.d/intranet-tunnel-nginx + nginx -s reopen
容器内 /app/logs/*.log程序自己lumberjack(见第三节)

reopen 不截断、不换 inode,因此不丢日志。 不要给 nginx 日志用 copytruncate,更不要两边都配轮转。

4. 本轮踩到的三个新坑

  1. dash 的 $(...) 里不能嵌 heredocSITE_ID=$(python3 - <<'PY' ... PY)静默失败

python 连跑都没跑。改为「python 把结果写文件、shell 再读」。

  1. SQLite 里 index 是保留字insert into sites (... index ...) 会报

near "index": syntax error,列名必须加双引号。

  1. logrotate 的 create 只接受用户名:写容器内 UID(create 0600 10001 10001

会报 unknown user '10001'跳过整条规则 —— 宿主机上并没有这个用户。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论