Files
inkwatch/deploy/nginx.host.conf.example
jkooe 088c9b34a0 refactor(deploy): 移除 nginx 容器,接入层改由宿主承担(常驻 5 → 4)
## 结果

四容器编排从 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 是拼接语义)在宿主反代上
  一样成立,只是换了文件。
2026-11-08 20:29:43 +08:00

151 lines
7.1 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ─────────────────────────────────────────────────────────────────────
# 墨观 · **宿主**反向代理配置模板(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;
}
}