# 支付平台运维手册 ## 服务器信息 - 公网 IP: `47.94.88.244`(`qcsj88888.com` / `api.qcsj88888.com` / `doc.qcsj88888.com` 三条 A 记录均指向此) - hostname: `iZ2zed18i1o6sivyqop2plZ` - 内网 IP: `172.20.205.37`(原手册所记,本次未复核) - 域名: `qcsj88888.com`、`api.qcsj88888.com`、`doc.qcsj88888.com` - 系统: Ubuntu (Alibaba Cloud ECS) > 这台机器在别处没有任何记录。排查时容易和 antom(`8.138.203.55`)、国内 credit-pay > (`47.107.96.60`)、美西(`132.148.75.28`)三台混淆——**它是独立的第四台。** ## 容器拓扑 | 容器 | 镜像(2026-09-24 实测) | 端口映射 | 网络(未复核) | |---|---|---|---| | java-backend | `pay:latest` ⚠️ | 0.0.0.0:8001→8001 | app-tier, deploy_app-tier | | frontend | `xjz/frontend:2.5.4` | 0.0.0.0:5173→80 | app-tier | | payment-doc | `xjz/payment-doc-server:1.0.0` | 0.0.0.0:7878→7878 | — | | postgres | `postgres:18.3` | 0.0.0.0:5432→5432 ⚠️ | 宿主机, deploy_app-tier | | redis | `redis:8.6.2-alpine` | 0.0.0.0:6379→6379 ⚠️ | deploy_app-tier | | nginx-proxy-manager | `jc21/nginx-proxy-manager:latest` | 0.0.0.0:80-81, 443 | deploy_app-tier | | gogs | `gogs/gogs:latest` | 0.0.0.0:3000, 2222 | deploy_app-tier | > ⚠️ **两处与旧版手册不符,上服务器前先核实:** > > 1. **java-backend 实际跑的是 `pay:latest`**,不是手册和仓库 compose 里写的 > `xjz/java-backend:1.0.0`。照「升级部署」一节构建 `xjz/java-backend:1.0.0` 再 > `docker compose up -d java-backend`,**很可能不会替换掉正在运行的容器**。服务器上的 > compose 与仓库里的 `docker-compose.yml` 已经不同步,部署前先比对。 > 2. **postgres / redis 绑的是 `0.0.0.0`,不是 `127.0.0.1`。** 目前靠阿里云安全组挡着 > (2026-09-24 从公网实测 5432 / 6379 均不可达),但容器层面是无保护的, > 安全组一旦放宽就直接暴露。建议改成 `127.0.0.1:5432:5432` 这类回环绑定。 ``` qcsj88888.com (HTTPS) ↓ NPM ├── /api/v1/* → java-backend:8001 └── 其余 → frontend:5173 ``` ## 项目路径 - 代码: `/root/payments/code/payment-platform` - Compose: `/root/payments/code/payment-platform/docker-compose.yml` - NPM 数据: `/root/payments/deploy/nginx-proxy-manager/data` ## 常用操作 ### 查看容器状态 ```bash cd /root/payments/code/payment-platform docker compose ps docker stats --no-stream ``` ### 查看日志 ```bash docker compose logs java-backend --tail 50 # 最近50行 docker compose logs java-backend -f # 实时跟踪 docker compose logs java-backend --since 5m # 最近5分钟 ``` ### 重启后端 ```bash docker compose restart java-backend ``` ### 停止/启动 ```bash docker compose stop java-backend docker compose start java-backend ``` ### 健康检查 ```bash curl http://localhost:8001/api/v1/payment/notify/health # 预期返回: {"code":0,"data":{"status":"ok"}} ``` ## 升级部署 ```bash # === 本地 === cd D:\project2\payment-platform\java mvn clean package -DskipTests scp target/payment-platform-1.0.0.jar root@172.20.205.37:/root/payments/code/payment-platform/java/target/ # === 服务器 === cd /root/payments/code/payment-platform/java docker build -t xjz/java-backend:1.0.0 . cd .. docker compose up -d java-backend docker compose logs java-backend --tail 20 -f ``` ## 数据库迁移 新增 SQL 脚本放在 `java/sql/`,服务器执行: ```bash docker exec -i postgres psql -U admin -d payment_platform_prod < /root/payments/code/payment-platform/java/sql/xxx.sql ``` ## 回滚到 Python ```bash docker stop java-backend docker rm java-backend cd /root/payments/code/payment-platform git checkout master docker compose up -d backend ``` ## 配置说明 ### JVM 参数 ```yaml JAVA_OPTS: "-Xms128m -Xmx256m -XX:+UseSerialGC -XX:MaxRAMPercentage=50 -XX:TieredStopAtLevel=1" ``` - `-Xms128m`: 初始堆 128MB - `-Xmx256m`: 最大堆 256MB - `-XX:+UseSerialGC`: 单线程 GC(2C 服务器避免并发 GC 开销) - `-XX:MaxRAMPercentage=50`: JVM 最多用 50% 物理内存 - `-XX:TieredStopAtLevel=1`: 减少 JIT 编译层级,启动更快 ### 环境变量(数据库/Redis) ```yaml SPRING_DATASOURCE_DRUID_URL: jdbc:postgresql://postgres:5432/payment_platform_prod?currentSchema=public SPRING_DATASOURCE_DRUID_USERNAME: admin SPRING_DATASOURCE_DRUID_PASSWORD: 'xjz#123321' SPRING_DATA_REDIS_HOST: redis SPRING_DATA_REDIS_PORT: 6379 SPRING_DATA_REDIS_DATABASE: 0 SPRING_DATA_REDIS_PASSWORD: '123456#xjz' ``` ## 端口暴露(2026-09-24 公网实测) 从公网直连 `47.94.88.244` 的探测结果: | 端口 | 服务 | 公网 | 说明 | |---|---|---|---| | 80 / 443 | NPM | ✅ 开放 | 对外服务,正常 | | 81 | NPM 管理面板 | ❌ 不可达 | 安全组已挡,保持 | | 3000 | gogs | ⚠️ **开放** | 见下 | | 2222 | gogs SSH | ❌ 不可达 | | | 5432 | postgres | ❌ 不可达 | 靠安全组挡,容器绑的是 `0.0.0.0` | | 6379 | redis | ❌ 不可达 | 同上 | | 8001 | java-backend | ❌ 不可达 | 经 NPM 转发 | | 7878 | payment-doc | ❌ 不可达 | 经 NPM 转发 | **gogs 的 3000 是本机除 80/443 外唯一对公网开放的服务。** 若是有意为之(外部拉取代码) 则无需处理;若是无意的,建议安全组关掉,改由 NPM 反代到域名下。 --- ## SSL 证书 > 本节 2026-09-24 新增。当天三个域名 HTTPS 中断三天,根因与处置见下。 ### 机制 TLS 由 **nginx-proxy-manager**(NPM,容器名 `nginx-proxy-manager`)终结。宿主机上 只有它占 80/443。 | 域名 | 容器内代理配置 | 证书目录 | 证书名 | |---|---|---|---| | `qcsj88888.com` | `/data/nginx/proxy_host/2.conf` | `/etc/letsencrypt/live/npm-2/` | npm-2 | | `api.qcsj88888.com` | `/data/nginx/proxy_host/3.conf` | `/etc/letsencrypt/live/npm-3/` | npm-3 | | `doc.qcsj88888.com` | `/data/nginx/proxy_host/4.conf` | `/etc/letsencrypt/live/npm-4/` | npm-4 | 三个域名**各自一张独立证书**(NPM 一个 Proxy Host 一张,SAN 不合并)。 `live/` 下是符号链接,指向 `archive/npm-/` 里的实际文件。 宿主机数据目录:`/root/payments/deploy/nginx-proxy-manager/data/` (证书**不在**这里面,在容器内 `/etc/letsencrypt`)。 ### 校验方式:DNS-01(NPM 原生功能,非定制改造) 证书走 **DNS challenge + dns-01**,认证器是 certbot 的阿里云插件: ``` Requested authenticator dns-aliyun Var pref_challs=['dns-01'] Var dns_aliyun_credentials=/etc/letsencrypt/credentials/credentials-* ``` **这是 NPM 自带的功能,不要误判成定制改造。** 可选 provider 列在 `/app/certbot/dns-plugins.json`(上游路径 `backend/certbot/dns-plugins.json`), provider 的**显示名从该文件读取**,所以日志里会出现 `via Aliyun` 这种拼接文案—— 这是正常的,不是补丁痕迹。 镜像来源确认(2026-09-24): ``` Created: 2026-02-17T05:43:40Z RepoDigests: jc21/nginx-proxy-manager@sha256:2aa69b382a384b676c0d4f1d6f2eac40ecd478fcf7af1cfb3f9f1d3cd0c81e12 ``` **所以这台能直连 Let's Encrypt。** 不要把 antom 那台"必须用 ZeroSSL(国际出口不通)" 的结论套到这台上——两台出网情况不同。 ### 续期 NPM 每小时 `:27:42` 跑一次检查,把**所有**已进入「到期前 30 天」窗口的证书**串行**续掉。 三张分散在几个整点完成,是因为各自 `expires_on` 差了几小时、跨过窗口的时刻不同, 不是「每次只续一张」。 > ⚠️ **续期成功后 NPM 不会 reload nginx**,根因见下。 ```bash docker logs nginx-proxy-manager 2>&1 | grep -i 'SSL' ``` ### 验证:看实际发出的证书,不要只看文件 ```bash # 服务器本机(127.0.0.1 + SNI,绕开 DNS 缓存) echo | openssl s_client -connect 127.0.0.1:443 -servername qcsj88888.com 2>/dev/null | openssl x509 -noout -dates ``` ```bash # 外网视角(在任一有国际出口的机器上跑) curl -sS -o /dev/null -w 'verify=%{ssl_verify_result}\n' https://qcsj88888.com/ ``` `verify=0` 表示客户端校验通过。 **为什么必须这样验:** 2026-09-24 那次,磁盘上证书完全正常(`certbot certificates` 报 `VALID: 59 days`),但 nginx 一直发 6/25 签发的旧证书。**只扫证书文件的到期检查 会报"一切正常",而生产已经挂了。** ### 装载故障的处置 症状:磁盘证书是新的,公网发出的却是旧的。此时**不要重签**(浪费 CA 配额且治不好)。 ```bash docker exec nginx-proxy-manager nginx -t ``` 必须先输出 `syntax is ok` + `test is successful` 才能继续。证书与私钥不匹配时 `nginx -t` 会拦下——**绝不让 nginx 加载坏配置**(9-13 antom 那次就是栽在 "新证书配旧私钥"上,reload 后 nginx 起不来、域名全挂)。 ```bash docker exec nginx-proxy-manager nginx -s reload ``` ### 根因(2026-09-24 查清):续期路径不 reload nginx **NPM 只在「签发新证书」时 reload nginx,定时续期这条路线上根本没有这一步。** 全 `/app` 范围内 `Nginx.reload()` 的调用点(`grep -rn "Nginx.reload()" /app/`): ``` proxy-host.js:296,397 redirection-host.js:294,395 access-list.js:201,305 stream.js:243,344 dead-host.js:242,324 ip_ranges.js:113 setting.js:51,63 certificate.js:155,158,164,171,177,184 nginx.js:94(定义处) migrations/2021...regenerate_default_host.js:22 ``` 全是 CRUD 流程。`certificate.js` 那 6 处在 `insertAndFetch(data)` 之后,属于 **POST /certificates(签发)**,服务于「临时禁用 host → 签发 → 恢复 host」。 续期链路一次都没有: ``` processExpiringHosts (:49) 定时任务,查 expires_on 进入 30 天窗口的证书 → renew (:893) 串行 forEach 调用 → renewLetsEncryptSsl (:929) http-01 → renewLetsEncryptSslWithDnsChallenge (:970) dns-01 → utils.execFile(certbotCommand, args, adds.opts) → logger.info(result) → return result ``` 两个 renew 函数体里只有 `execFile` 和 `logger.info`,不 import nginx 模块,也没有 reload。 **代码与现象互相印证:** 8/25 续期日志显示 certbot 成功、数据库写入,全程无 reload 记录、 无报错 —— 不是「reload 步骤丢了」,是这条路径上本来就没有。 **为什么只有这台暴露:** reload 只在**改配置**时发生(动 NPM 界面会顺带把新证书带进来)。 这台三个月没人碰过 NPM 配置,`database.sqlite` 上一次写入就是 8/25 的续期本身, 所以 nginx 从 6/25 签发后再没重载过。 > 未核实上游是否已在更新版本中修掉。可确认的事实只有:**这个 digest > (`sha256:2aa69b38…`)的镜像,续期路径不 reload。** ### 复发时间线与处置建议 NPM 在到期前 30 天续期,即约 **10/23** 再次静默续期。届时若不干预,nginx 仍发 8/24 那张, **11/22 三个域名再次过期**。 建议处置(**尚未实施**):宿主机加定时自愈 —— 比对「实际发出的证书」与「磁盘当前证书」, 不一致就 `nginx -t && nginx -s reload`,并推钉钉告警。 **不要改镜像。** `/app/internal/certificate.js` 是控制面代码,改错会让 `nginx-proxy-manager` 起不来,连带 80/443 全挂 —— 而 80/443 正是用来签发和续期的入口。 --- ## 故障排查 ### 后端起不来 ```bash docker compose logs java-backend --tail 100 ``` 常见原因: - `Connection refused: postgres:5432` → postgres 没加入 deploy_app-tier,执行 `docker network connect deploy_app-tier postgres` - `OutOfMemoryError` → 调大 `-Xmx` 到 384m - 端口占用 → `lsof -i:8001`,停掉占用进程 ### 页面访问 502 检查后端是否正常: ```bash curl http://localhost:8001/api/v1/payment/notify/health ``` 后端正常则查 NPM 是否在线:`docker ps | grep nginx-proxy-manager` ### 磁盘空间 ```bash docker system prune -a --filter "until=72h" # 清理72小时前的旧镜像 ``` ## 备份 ```bash # 数据库 docker exec postgres pg_dump -U admin payment_platform_prod > /root/backup/payment_$(date +%Y%m%d).sql ```