## 结果 四容器编排从 6 个 service 降到 5 个:`postgres` / `redis` / `business` / `gateway` 常驻 + `migrate` 一次性。**常驻容器 5 → 4**,实测直连 `127.0.0.1:4000|3000/health` 均 200。 ## 为什么现在做 `docker-compose.prod.yml` 里早就写着"生产不启动自带 nginx 容器,接入层交给 1Panel OpenResty",只是用 `profiles: ["never"]` 停用的。也就是说**生产本来就没这个容器**, 默认文件里留着它反而让两条路径分叉:本地开发跑一套、生产跑另一套,`nginx.conf` 只服务前者。现在两套文件对齐,反代只在宿主一处。 ## 三个必须处理的连带问题(都不是删几行就完事) **① `&logging` 锚点原本挂在 nginx service 里**,被另外 5 个 service 用 `*logging` 引用。直接删块会让 Compose 报 YAML 锚点未定义,而**报错点在引用的那几行、不在 被删的地方**。已把它提到顶层 `x-logging:` 扩展字段(Compose 支持),并把这个坑 写进注释。 **② prod 覆盖里那段 `nginx: profiles: ["never"]` 会变成悬空引用**(服务已不存在), 一并删除并改写注释。 **③ `watchdog.sh` 探的 `/healthz` 原本是 nginx 容器提供的虚拟端点。** 删掉后它会 恒报 `entry:404` —— 一个"服务其实好好的但监控一直叫"的假故障。已把探针语义改成 "接入层活着",并在 404 时把最常见成因写进告警文本(宿主反代没配 /healthz)。 ## 宿主反代模板 `deploy/nginx.host.conf.example`(替代原来的 `deploy/nginx.conf`),关键差异: upstream 从 `gateway:3000` / `business:4000` 改为 `127.0.0.1:3000` / `127.0.0.1:4000` —— 容器服务名**在宿主上根本不解析**,照抄旧配置会 `host not found in upstream`。 四条承载性指令原样保留,且写清了缺每一条的**症状**(都不是 502): - `proxy_http_version 1.1` + `Upgrade`/`Connection` → 缺了 WebSocket 握手失败 - `proxy_read_timeout 600s` → 缺了长连接 60 秒被无声掐断 - `proxy_buffering off` → 缺了画面不实时(数据被攒住) - `/relay/` 的 `proxy_request_buffering off` → 缺了"文件不落盘"变成假话 配置已用真 `nginx -t` 校验(`nginx:1.27-alpine`,`syntax is ok / test is successful`), 不是靠肉眼或 grep 判断。 ## 护栏:把"搬到宿主"这件事本身变成可验证的 `tests/dockerfile-coverage-test.js` 新增 7 条:模板存在、四条承载性指令各一条、 upstream 必须是回环而非 Docker 服务名、以及一条**负向**断言 —— 四容器编排里不许 再冒出 `nginx:` 服务(防"顺手加回来"造成两处真相)。 不加这段的话,宿主模板被删或被简化**不会有任何测试变红**,而症状是"页面能开但画面 不刷新",从现象很难倒推回配置。 测试:`dockerfile-coverage` **60/0**、`claim-proof` **57/0**。 ## README 现状描述(目录树 / 端口表 / 起栈步骤)已改。**历史段落不改写**: - "5 容器 healthy、经 nginx :80 入口"那句加"时代注记",说明那是当时形态; - "坑 B:nginx 强依赖证书"整段加抬头说明容器已移除,但**过程与教训原样保留** —— 那两条坑(硬引用可选文件导致 clone 必崩、Compose ports 是拼接语义)在宿主反代上 一样成立,只是换了文件。
151 lines
7.1 KiB
Plaintext
151 lines
7.1 KiB
Plaintext
# ─────────────────────────────────────────────────────────────────────
|
||
# 墨观 · **宿主**反向代理配置模板(2026-11-08 起)
|
||
#
|
||
# ⚠️ 这不是容器内配置。2026-11-08 已从四容器编排里**移除 nginx 容器**,
|
||
# 接入层一律由宿主承担。所以本文件里的 upstream 是 `127.0.0.1:3000/4000`
|
||
# (compose 用 `!override` 把两个端口绑回环,见 docker-compose.prod.yml),
|
||
# **不是** 原来的 `gateway:3000` / `business:4000` —— 那两个名字只在
|
||
# Docker 网络内部可解析,宿主 nginx 用它必然 `host not found in upstream`。
|
||
#
|
||
# 两种用法:
|
||
# ① 裸 nginx 做接入层:整份照抄(改掉 server_name 与证书路径即可)。
|
||
# ② 1Panel / 已有 OpenResty 站点:**只抄 location 段**,把它贴进站点的
|
||
# 反向代理配置里,`listen` / `server_name` 用面板自己的。
|
||
#
|
||
# 四条**承载性**指令(缺一条不是 502,而是更难看出来的症状):
|
||
# · `proxy_http_version 1.1` + `Upgrade`/`Connection` 头 → 缺了 WebSocket 握手失败
|
||
# · `proxy_read_timeout 600s` → 缺了长连接 60 秒被无声掐断
|
||
# · `proxy_buffering off` → 缺了画面不实时(数据被攒住)
|
||
# · `/relay/` 的 `proxy_request_buffering off` → 缺了"文件不落盘"变成假话
|
||
# (请求体被 nginx 缓冲到临时文件)
|
||
# ─────────────────────────────────────────────────────────────────────
|
||
|
||
# 墨观 接入层配置(见方案 V4.0 第 9 章 / 第 11 章)
|
||
#
|
||
# 两种模式:
|
||
# 1) 默认(本文件)—— 仅 HTTP,开箱可用,适合本地开发与内网部署。
|
||
# 2) 生产 HTTPS —— 把本文件替换为 nginx.ssl.conf.example,
|
||
# 并将证书放入 deploy/certs/(fullchain.pem / privkey.pem)。
|
||
#
|
||
# 注意:upstream 使用 compose 服务名(gateway / business),
|
||
# 而非 127.0.0.1 —— nginx 与后端同处 compose 桥接网络。
|
||
|
||
upstream inkwatch_gateway {
|
||
# 多实例扩容时在此增加后端(见方案 11.3)
|
||
server 127.0.0.1:3000 max_fails=2 fail_timeout=10s;
|
||
keepalive 64;
|
||
}
|
||
|
||
upstream inkwatch_business {
|
||
server 127.0.0.1:4000 max_fails=2 fail_timeout=10s;
|
||
keepalive 16;
|
||
}
|
||
|
||
# WebSocket 连接升级所需的映射
|
||
map $http_upgrade $connection_upgrade {
|
||
default upgrade;
|
||
'' close;
|
||
}
|
||
|
||
server {
|
||
listen 80 default_server;
|
||
server_name _;
|
||
|
||
# 容器探活端点:必须放在最前且不被重定向
|
||
location = /healthz { return 200 'ok'; add_header Content-Type text/plain; }
|
||
|
||
# 强制 WSS:拒绝明文 WebSocket(见方案 9.1)
|
||
location = /health {
|
||
proxy_pass http://inkwatch_gateway/health;
|
||
proxy_set_header Host $host;
|
||
}
|
||
|
||
# ── 文件中继(不落盘)────────────────────────────────────────────
|
||
# ★ 必须关掉请求体/响应缓冲:nginx 默认会把请求体**缓冲到临时文件**,
|
||
# 那样"服务端不落盘"就是假的 —— 文件其实落到了 nginx 的 temp 上。
|
||
# 这一条是这个功能成立的前提,不是性能调优。
|
||
location ^~ /relay/ {
|
||
proxy_pass http://inkwatch_gateway;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
|
||
proxy_request_buffering off;
|
||
proxy_buffering off;
|
||
client_max_body_size 2048m;
|
||
proxy_read_timeout 600s;
|
||
proxy_send_timeout 600s;
|
||
proxy_connect_timeout 10s;
|
||
}
|
||
|
||
# Socket.io 长连接
|
||
location /socket.io/ {
|
||
proxy_pass http://inkwatch_gateway;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Upgrade $http_upgrade;
|
||
proxy_set_header Connection $connection_upgrade;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
|
||
# 长连接关键参数:超时放宽,禁用缓冲
|
||
proxy_read_timeout 600s;
|
||
proxy_send_timeout 600s;
|
||
proxy_connect_timeout 10s;
|
||
proxy_buffering off;
|
||
}
|
||
|
||
# 云端管理台(静态页 + /api/admin/*)。
|
||
# 与 /api/ 同源代理到 business:静态页由业务服务 express.static 提供,
|
||
# 这样单容器与多容器两套部署的路径完全一致(否则要把 admin 目录
|
||
# 分别 COPY 进两种 nginx 镜像,早晚漏一处)。
|
||
#
|
||
# ⚠️ 管理台默认只该在内网/VPN 可达。本文件不管这件事 —— 请用安全组
|
||
# 限制来源,或设 ADMIN_ALLOW_IPS。这一条只是路由,不是访问控制。
|
||
location ^~ /admin/ {
|
||
proxy_pass http://inkwatch_business;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
# 管理页不该被任何中间层缓存(含 CDN / 反向代理)
|
||
proxy_no_cache 1;
|
||
# 这两个头后端(admin.js 的 pageGuard/apiGuard)也会设,这里再设一遍是**有意的**:
|
||
# nginx 的 add_header 是追加而非覆盖,重复值语义无害(no-store, no-store);
|
||
# 而 `always` 让 4xx/5xx 响应也带上 —— 后端出错时那条响应没有机会自己加头,
|
||
# 那正是最需要 no-store 的时刻(别让中间层把一个 500 缓存住)。
|
||
add_header Cache-Control "no-store" always;
|
||
add_header X-Robots-Tag "noindex, nofollow" always;
|
||
}
|
||
|
||
# 安装向导(首次初始化用)。与 /admin/ 同样由业务服务提供 ——
|
||
# 它要读库、要写库,交给 nginx 托管静态文件没有意义。
|
||
# 初始化完成后服务端会把它变成 404(判据是库里的 bootstrap_state)。
|
||
location ^~ /setup {
|
||
proxy_pass http://inkwatch_business;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
proxy_no_cache 1;
|
||
add_header Cache-Control "no-store" always;
|
||
add_header X-Robots-Tag "noindex, nofollow" always;
|
||
}
|
||
|
||
# 业务 REST 接口
|
||
location /api/ {
|
||
proxy_pass http://inkwatch_business;
|
||
proxy_http_version 1.1;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
client_max_body_size 256k;
|
||
}
|
||
}
|