宝塔与 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.cloud 与 barbershop.sushike.cloud, 再起一个容器抢 80/443 会直接冲突。
做法:只启三个服务,把 80/443 留给宝塔 nginx 作前置。
docker compose up -d tunnel-server postgres db-backup # 注意:不含 nginxnginx 配置位于 baota/tunnel.sushike.cloud.conf 与 baota/wildcard-t.sushike.cloud.conf, 安装到 /www/server/panel/vhost/nginx/(主配置第 96 行已 include 该目录)。
回归验证要点:barbershop.sushike.cloud.conf:140 有 listen 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 与现有记录互不冲突。
新增两条解析(均为新增,未改动任何现有记录):
| 记录 | 类型 | 值 |
|---|---|---|
tunnel | A | <公网IP> |
*.t | A | <公网IP> |
3. 内存:原 .env 的资源上限超过物理内存
.env 默认 SERVER_MEM_LIMIT=2g、DB_MEM_LIMIT=2g,而整机只有 1.5 GiB。 按实测下调(该机 available 约 500 MB):
| 变量 | 原值 | 实际值 |
|---|---|---|
SERVER_CPU_LIMIT / SERVER_MEM_LIMIT / SERVER_MEM_RESERVE | 2.0 / 2g / 256m | 1.0 / 512m / 64m |
DB_CPU_LIMIT / DB_MEM_LIMIT / DB_MEM_RESERVE | 2.0 / 2g / 256m | 1.0 / 320m / 96m |
4. 面板默认禁止外网访问,且该开关以数据库为准
.env 的 PANEL_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/backups2. bind mount 后属主变成 root,服务端起不来
容器以 USER tunnel(uid 10001)运行。匿名卷会继承镜像内目录的属主, bind mount 不会。不改属主的报错是:
服务端启动失败: cert: 写入 ACME 账户密钥失败: open certs/acme_account.key: permission denied首次部署必做:
mkdir -p certs backups logs && chown -R 10001:10001 certs backups logs3. /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。 若嫌大,改.env的LOG_FILE_MAX_SIZE=20、LOG_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-48100 | TCP 隧道端口池 | 本次新增 | 本次新增 |
| 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 的两个传参坑(用本工具部署时必踩)
- 远程命令里以
-开头的 token 会被 workbench 当成自己的 flag。
报错形如 bad flag syntax: --- 或 unknown shorthand flag: 'l' in -l(后者来自 wc -l)。 PowerShell 5.1 把多行字符串传给原生程序时会拆分参数。 对策:把远程脚本写成文件上传,再用 sh /path/script.sh 这种极简命令执行。
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.db 里 sites 表的 id。 站点不在库里 → 没有 ID → 统计无从归属。
手写 conf 建立的站点必须补三件事:
- 在
site.db的sites+domain表注册站点,拿到 ID - 建
extension/<域名>/site_total.conf,写入带该 ID 的access_log syslog:... - 在该站点 conf 里
include这个 extension 目录
本项目站点已注册为 ID=3(tunnel.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. 本轮踩到的三个新坑
- dash 的
$(...)里不能嵌 heredoc:SITE_ID=$(python3 - <<'PY' ... PY)会静默失败,
python 连跑都没跑。改为「python 把结果写文件、shell 再读」。
- SQLite 里
index是保留字:insert into sites (... index ...)会报
near "index": syntax error,列名必须加双引号。
- logrotate 的
create只接受用户名:写容器内 UID(create 0600 10001 10001)
会报 unknown user '10001' 并跳过整条规则 —— 宿主机上并没有这个用户。
