合并 088c9b3(移除 nginx 容器、常驻 5→4)到 main。三路合并零文本冲突,但 088c9b3 漏改了引用被重命名文件的地方,故在本合并提交里一并适配:
1) tests/phase3b-static-guard.js:265 的断言原本读 deploy/nginx.conf,该文件已 rename 成 deploy/nginx.host.conf.example;不改会让 read() 返回空串、断言必然 FAIL,而该护栏被 run-guards.sh / Gitea Actions 收录。新模板 108-109 行确有 location ^~ /admin/ 与 proxy_pass,换名后断言成立。
2) deploy/nginx.ssl.conf.example 的使用说明仍写 cp nginx.conf 与 docker compose up -d nginx(两者都已不存在),改为时代注记,点明本文件只是 HTTPS 片段、缺 /admin/ /setup /relay 段、不可整份照抄(纯注释,不改配置行为)。
验证:bash tests/run-guards.sh → 6 套件 PASS 5 / FAIL 0 / SKIPPED 1(axe 可选未启用);phase3b 94/0、dockerfile-coverage 60/0、schema 116/0、Dart 674 全通过,整体退出码 0。
This commit is contained in:
commit
49dacb57a6
8 files changed
+121
-67
No files matched your search
@@ -55,10 +55,10 @@ inkwatch/
|
||||
│ ├── lib/ui/ # 界面层(样式只许来自 ui/theme.dart,有护栏测试)
|
||||
│ └── test/ # Dart 测试(含视觉令牌护栏与跨语言角色目录校验)
|
||||
├── deploy/
|
||||
│ ├── docker-compose.yml # 编排:nginx + gateway + business + redis + pg
|
||||
│ ├── docker-compose.yml # 编排:gateway + business + redis + pg
|
||||
│ ├── Dockerfile.gateway # 多阶段构建(ADR-001)
|
||||
│ ├── Dockerfile.business
|
||||
│ ├── nginx.conf # WSS 反代 / 长连接调优 / /admin/ 路由
|
||||
│ ├── nginx.host.conf.example # **宿主**反代模板(WSS / 长连接 / /admin/ 路由)
|
||||
│ ├── nginx-all.conf # 单容器统一入口(3500)
|
||||
│ └── init.sql # TimescaleDB 超表 / 保留策略 / 降采样(含增量迁移段)
|
||||
└── tests/
|
||||
@@ -106,11 +106,16 @@ inkwatch/
|
||||
cd deploy
|
||||
docker compose up -d --build
|
||||
|
||||
docker compose ps # 5 个容器应全部 healthy
|
||||
curl http://localhost/healthz # ok
|
||||
docker compose ps # 4 个容器应全部 healthy
|
||||
curl http://127.0.0.1:4000/health # ok
|
||||
```
|
||||
|
||||
默认仅 HTTP,开箱可用。启用 HTTPS 见 `deploy/nginx.ssl.conf.example`。
|
||||
> **接入层不在容器里**(2026-11-08 起)。原先的 nginx 容器已移除:那台机的 80/443
|
||||
> 本来就被 1Panel OpenResty 占着,自带 nginx 只是多一跳。现在反代由宿主承担,
|
||||
> 模板见 `deploy/nginx.host.conf.example`(裸 nginx 整份照抄;1Panel 只抄 location 段)。
|
||||
> 四条承载性指令必须抄全(WebSocket 升级头 / `proxy_read_timeout 600s` /
|
||||
> `proxy_buffering off` / `/relay/` 的 `proxy_request_buffering off`)——
|
||||
> 少一条的表现不是 502,而是"能开页面但画面不刷新"或"文件其实落了盘"。
|
||||
|
||||
**无 TimescaleDB 镜像时**(国内拉取 1GB 级镜像可能极慢):`init.sql` 已做成
|
||||
**扩展可缺省**,把 PG 换成普通的即可,表结构与指标列完全一致:
|
||||
@@ -131,10 +136,10 @@ POSTGRES_IMAGE=postgres:15-alpine docker compose up -d --build
|
||||
|
||||
| 服务 | 端口 | 用途 |
|
||||
|---|---|---|
|
||||
| nginx | 80 | 接入层(`/socket.io/` → 网关,`/api/` → 业务) |
|
||||
| gateway | 3000 | Socket.io 网关(调试直连用) |
|
||||
| business | 4000 | 业务 REST(调试直连用) |
|
||||
| gateway | 3000 | Socket.io 网关(宿主反代的上游) |
|
||||
| business | 4000 | 业务 REST(宿主反代的上游) |
|
||||
| postgres / redis | 5432 / 6379 | 仅 `127.0.0.1` 绑定 |
|
||||
| ~~nginx~~ | ~~80~~ | **已移除**(2026-11-08):反代归宿主,见上面的模板 |
|
||||
|
||||
> 凭据经 `.env` 注入,默认值为本地开发用。生产务必覆盖 `POSTGRES_PASSWORD`。
|
||||
|
||||
@@ -318,6 +323,8 @@ node perf-bench.js # ★ 性能基准(动网关热路径前后
|
||||
> 该文件头部早就写了这条教训("标签写 416 而实跑已 598"),仍在复发,
|
||||
> 故这次连头部注释一起校正。
|
||||
上表的服务端套件已在 **Docker 全栈**(5 容器 healthy、经 nginx `:80` 入口)上复跑通过,
|
||||
(**时代注记**:那一轮跑的是 5 容器形态。2026-11-08 移除 nginx 容器后为 4 容器 +
|
||||
宿主反代;上述数字本身未因此变化,但"经 nginx `:80` 入口"这句只对当时成立。)
|
||||
非仅本机进程直跑。
|
||||
|
||||
**真实帧实测值**(`rust-frame-pipeline-test.js` 打印,本机 M3 Max):
|
||||
@@ -458,6 +465,11 @@ disk_free_gb=3251.79 net_rx_mbps=4.55 net_tx_mbps=1.71 uptime_s=106001
|
||||
`nginx.conf` 的 upstream 也从 `127.0.0.1:3000` 改为 `gateway:3000`。
|
||||
(host 网络确实能省一层 NAT,但与 compose 服务发现互斥,单机也要二选一。)
|
||||
|
||||
> ⚠️ **本节的 nginx 容器已于 2026-11-08 移除**(接入层改由宿主承担,见
|
||||
> `deploy/nginx.host.conf.example`)。下面两条坑仍值得读 —— 它们的教训是
|
||||
> "配置硬引用可选文件会让 clone 下来必崩"与"ports 是拼接而非替换",
|
||||
> 这两条在宿主反代上一样成立,只是换了文件。
|
||||
|
||||
### 坑 B:nginx 强依赖证书导致「clone 下来必崩」
|
||||
|
||||
`certs/` 被 `.gitignore` 排除(私钥不能入库),但 nginx.conf 硬引用
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
#
|
||||
# ① 该机 80/443 已被 1Panel OpenResty 占用(且 OpenResty 跑在 host 网络,
|
||||
# 反代的是 127.0.0.1:端口 —— 既有 searxng 就是这么接的)。
|
||||
# → 生产**不启动** 墨观 自己的 nginx 容器,接入层交给 1Panel 站点。
|
||||
# → 代价:deploy/nginx.conf 里那几条承载性指令必须搬进 1Panel 站点配置
|
||||
# (WebSocket 升级头 / proxy_read_timeout 600s / proxy_buffering off /
|
||||
# proxy_http_version 1.1)—— 按上述四条搬进 1Panel 站点配置。
|
||||
# → **两套 compose 文件里都不再有 nginx 容器**(2026-11-08 移除,本文件原先
|
||||
# 只是用 profiles 停用它)。接入层一律交给宿主:1Panel 站点,或裸 nginx。
|
||||
# → 宿主配置模板见 deploy/nginx.host.conf.example。必须照抄的四条承载性指令:
|
||||
# WebSocket 升级头 / proxy_read_timeout 600s / proxy_buffering off /
|
||||
# proxy_http_version 1.1 —— 少任何一条的表现都不是 502,而是
|
||||
# "能打开页面但画面不刷新"或"WebSocket 60 秒后被无声掐断"。
|
||||
#
|
||||
# ② 该实例云防火墙是「全部 TCP 1-65535 放通」。
|
||||
# → gateway(3000) / business(4000) **必须**绑回环:业务服务带
|
||||
@@ -24,11 +26,6 @@
|
||||
name: inkwatch
|
||||
|
||||
services:
|
||||
# ---------- 接入层:由 1Panel OpenResty 承担,生产不启动本容器 ----------
|
||||
# 用 profile 停用(而不是删掉本服务)—— 本地开发仍按默认文件起它。
|
||||
nginx:
|
||||
profiles: ["never"]
|
||||
|
||||
gateway:
|
||||
ports: !override
|
||||
- "127.0.0.1:3000:3000"
|
||||
|
||||
+11
-30
@@ -9,36 +9,18 @@
|
||||
# 永不随显示名改。
|
||||
name: inkwatch
|
||||
|
||||
services:
|
||||
# ---------- 接入层:反向代理 + WSS 升级 ----------
|
||||
# 默认仅 HTTP(开箱可用)。启用 HTTPS 见 deploy/nginx.ssl.conf.example
|
||||
nginx:
|
||||
image: nginx:1.27-alpine
|
||||
ports:
|
||||
- "80:80"
|
||||
# 启用 HTTPS 时取消下行注释,并把 nginx.conf 换成 ssl 模板
|
||||
# - "443:443"
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
# 证书目录可选:目录为空时默认 HTTP 配置不读取证书
|
||||
- ./certs:/etc/nginx/certs:ro
|
||||
- nginx_logs:/var/log/nginx
|
||||
depends_on:
|
||||
gateway:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
logging: &logging
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m" # 日志有界:防磁盘打满(见方案 ADR-001)
|
||||
max-file: "3"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1/healthz"]
|
||||
interval: 20s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
# ── 日志有界(防磁盘打满,见方案 ADR-001)───────────────────────────
|
||||
# 用顶层 `x-` 扩展字段挂锚点,而不是挂在某个 service 上:
|
||||
# 挂在 service 上的锚点会随那个 service 一起被删 —— 2026-11-08 去掉 nginx
|
||||
# service 时差点踩到:删块后 5 处 `*logging` 会全部解析失败,而报错点在
|
||||
# 引用的那几行,不在被删的地方。
|
||||
x-logging: &logging
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "10m"
|
||||
max-file: "3"
|
||||
|
||||
# ---------- 网关:无状态 Socket.io 转发 ----------
|
||||
services:
|
||||
gateway:
|
||||
build:
|
||||
# 上下文取仓库根:网关需引用 server/shared 与 shared/(协议+编解码)
|
||||
@@ -247,4 +229,3 @@ services:
|
||||
volumes:
|
||||
redis_data:
|
||||
pg_data:
|
||||
nginx_logs:
|
||||
@@ -1,3 +1,25 @@
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# 墨观 · **宿主**反向代理配置模板(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 章)
|
||||
#
|
||||
# 两种模式:
|
||||
@@ -10,12 +32,12 @@
|
||||
|
||||
upstream inkwatch_gateway {
|
||||
# 多实例扩容时在此增加后端(见方案 11.3)
|
||||
server gateway:3000 max_fails=2 fail_timeout=10s;
|
||||
server 127.0.0.1:3000 max_fails=2 fail_timeout=10s;
|
||||
keepalive 64;
|
||||
}
|
||||
|
||||
upstream inkwatch_business {
|
||||
server business:4000 max_fails=2 fail_timeout=10s;
|
||||
server 127.0.0.1:4000 max_fails=2 fail_timeout=10s;
|
||||
keepalive 16;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
# PMon 接入层 · 生产 HTTPS 配置模板
|
||||
# InkWatch 接入层 · 生产 HTTPS 配置片段(宿主反代)
|
||||
#
|
||||
# ⚠️ 时代注记(2026-11-08):四容器编排已移除自带的 nginx 容器,接入层改由
|
||||
# 宿主(1Panel / OpenResty)承担,仓库里不再有 deploy/nginx.conf。
|
||||
# 配置入口以 deploy/nginx.host.conf.example 为准;本文件只补 HTTPS/证书相关段,
|
||||
# 缺 /admin/、/setup、/relay/ 等 location,需与宿主模板合并使用,不要整份照抄。
|
||||
#
|
||||
# 使用方法:
|
||||
# 1) 将证书放入 deploy/certs/:
|
||||
# deploy/certs/fullchain.pem
|
||||
# deploy/certs/privkey.pem
|
||||
# 2) 备份默认配置并启用本模板:
|
||||
# cd deploy
|
||||
# cp nginx.conf nginx.conf.bak
|
||||
# cp nginx.ssl.conf.example nginx.conf
|
||||
# 3) 重启接入层:
|
||||
# docker compose up -d nginx
|
||||
# 1) 将证书放到宿主 nginx 可读的目录(本文件按 /etc/nginx/certs/ 引用):
|
||||
# fullchain.pem / privkey.pem
|
||||
# 2) 备份宿主站点配置,把本片段并入,再校验并重载:
|
||||
# nginx -t && nginx -s reload
|
||||
#
|
||||
# 证书目录已被 .gitignore 排除,切勿把私钥提交入库。
|
||||
|
||||
upstream inkwatch_gateway {
|
||||
server gateway:3000 max_fails=2 fail_timeout=10s;
|
||||
server 127.0.0.1:3000 max_fails=2 fail_timeout=10s;
|
||||
keepalive 64;
|
||||
}
|
||||
|
||||
upstream inkwatch_business {
|
||||
server business:4000 max_fails=2 fail_timeout=10s;
|
||||
server 127.0.0.1:4000 max_fails=2 fail_timeout=10s;
|
||||
keepalive 16;
|
||||
}
|
||||
|
||||
|
||||
+13
-2
@@ -7,7 +7,12 @@
|
||||
# 并且只在**状态变化时**写(见 STATE 文件),不刷屏。
|
||||
#
|
||||
# ── 检查什么 ────────────────────────────────────────────────────────
|
||||
# 1. 统一入口 /healthz —— nginx 进程活着(最轻的判断)
|
||||
# 1. 统一入口 /healthz —— **接入层**活着(最轻的判断)
|
||||
# 2026-11-08 起接入层不在容器里(nginx 容器已移除),/healthz 由**宿主反代**
|
||||
# 提供(见 deploy/nginx.host.conf.example 的 `location = /healthz`)。
|
||||
# 语义因此弱了一层:它证明的是"人能访问到的第一跳在",而不是"容器内的 nginx
|
||||
# 进程在"。好处是它更接近用户真实路径;坏处是**宿主反代没配时这条必红** ——
|
||||
# 那正是它该报的,因为此时服务对外确实不可达。
|
||||
# 2. 网关 /health —— gateway 能应答(含 pg/redis 连通性字段)
|
||||
# 业务 /api/ 不单独探:它没有 health 路由(404 也说明 business 活着),
|
||||
# 用 404 当"活"会掩盖真故障,故改由网关 /health 的 checks 字段覆盖。
|
||||
@@ -35,7 +40,13 @@ code_of() {
|
||||
now() { date '+%Y-%m-%d %H:%M:%S%z'; }
|
||||
|
||||
fail=""
|
||||
c=$(code_of "$BASE/healthz"); [ "$c" = "200" ] || fail="$fail entry:$c"
|
||||
c=$(code_of "$BASE/healthz")
|
||||
# 404 的常见成因不是服务挂了,而是**宿主反代没配 /healthz**(删掉 nginx 容器后
|
||||
# 它就是宿主的事了)。把这一句写进告警文本,省掉下次排查的第一步。
|
||||
if [ "$c" != "200" ]; then
|
||||
fail="$fail entry:$c"
|
||||
[ "$c" = "404" ] && fail="$fail(宿主反代缺 /healthz?见 deploy/nginx.host.conf.example)"
|
||||
fi
|
||||
c=$(code_of "$BASE/health"); [ "$c" = "200" ] || fail="$fail gateway:$c"
|
||||
|
||||
prev="up"
|
||||
|
||||
@@ -568,7 +568,7 @@ console.log('\n=== 安全开关通路(代码 → .env.example → compose)==
|
||||
{
|
||||
file: 'deploy/docker-compose.yml', label: '四容器编排',
|
||||
map: { gateway: 'gateway', business: 'business' },
|
||||
selfCheck: ['gateway', 'business', 'redis', 'postgres', 'nginx'],
|
||||
selfCheck: ['gateway', 'business', 'redis', 'postgres'],
|
||||
// 四容器路径里网关端口由 compose 注入
|
||||
portSelfCheck: (envBySvc) => ok('compose 解析自检:gateway 的 PORT 被认出',
|
||||
!!(envBySvc.gateway && envBySvc.gateway.has('PORT'))),
|
||||
@@ -583,11 +583,41 @@ console.log('\n=== 安全开关通路(代码 → .env.example → compose)==
|
||||
},
|
||||
];
|
||||
|
||||
// ── 接入层已移到宿主(2026-11-08 移除 nginx 容器)─────────────────────
|
||||
// 为什么必须有这一段:nginx 容器删掉后,"反代该做什么"就只存在于**宿主**配置里,
|
||||
// 而宿主配置不在本仓的运行路径上 —— 一旦那个模板被删或被简化,没有任何测试会红,
|
||||
// 而症状也不是 502,是"页面能打开但画面不刷新""长连接 60 秒后被无声掐断"
|
||||
// "文件其实落到了 nginx 的 temp 上"。这类退化只能靠对模板本身断言来防。
|
||||
console.log('\n--- 接入层:宿主反向代理(容器已移除)---');
|
||||
const hostConf = path.join(ROOT, 'deploy/nginx.host.conf.example');
|
||||
ok('deploy/nginx.host.conf.example 存在', fs.existsSync(hostConf));
|
||||
if (fs.existsSync(hostConf)) {
|
||||
const nc = fs.readFileSync(hostConf, 'utf8');
|
||||
ok('宿主模板含 WebSocket 升级(http_version 1.1 + Upgrade 头)',
|
||||
/proxy_http_version\s+1\.1/.test(nc) && /Upgrade\s+\$http_upgrade/.test(nc));
|
||||
ok('宿主模板含 proxy_read_timeout 600s(长连接不被 60 秒掐断)',
|
||||
/proxy_read_timeout\s+600s/.test(nc));
|
||||
ok('宿主模板含 proxy_buffering off(画面才实时)',
|
||||
/proxy_buffering\s+off/.test(nc));
|
||||
ok('宿主模板含 proxy_request_buffering off(文件中继不落盘)',
|
||||
/proxy_request_buffering\s+off/.test(nc));
|
||||
// upstream 必须是宿主能解析的地址。容器服务名(gateway:3000)只在 Docker
|
||||
// 网络内可解析,宿主 nginx 用它必然 "host not found in upstream"。
|
||||
ok('宿主模板 upstream 用回环地址而非 Docker 服务名',
|
||||
/server\s+127\.0\.0\.1:3000/.test(nc) && /server\s+127\.0\.0\.1:4000/.test(nc)
|
||||
&& !/server\s+gateway:3000/.test(nc) && !/server\s+business:4000/.test(nc));
|
||||
}
|
||||
// 负向断言:四容器编排里不该再出现 nginx 服务。
|
||||
// 防的是"顺手加回来"——那会让接入层变成两处真相,且默认文件与 prod 覆盖
|
||||
// 再次分叉(prod 原先正是用 profiles 停用它)。
|
||||
const fourTxt = fs.readFileSync(path.join(ROOT, 'deploy/docker-compose.yml'), 'utf8');
|
||||
ok('四容器编排不含 nginx 服务(接入层只在宿主)',
|
||||
!/^\s{2}nginx:\s*$/m.test(fourTxt));
|
||||
|
||||
for (const cf of COMPOSE_FILES) {
|
||||
console.log(`\n--- ${cf.label}(${cf.file})---`);
|
||||
ok(`${cf.file} 存在`, fs.existsSync(path.join(ROOT, cf.file)));
|
||||
const composeText = fs.readFileSync(path.join(ROOT, cf.file), 'utf8');
|
||||
const envBySvc = composeEnvByService(composeText);
|
||||
const composeText = fs.readFileSync(path.join(ROOT, cf.file), 'utf8'); const envBySvc = composeEnvByService(composeText);
|
||||
const svcNames = Object.keys(envBySvc);
|
||||
|
||||
// 解析自检:解析失手会静默放行,故必须先证明解析是对的
|
||||
|
||||
@@ -262,7 +262,8 @@ section('E. Dockerfile / nginx 静态资源覆盖');
|
||||
ok(f + ' 整目录 COPY server/business/admin',
|
||||
/COPY[^\n]*server\/business\/admin[^\n]*\.\/server\/business\/admin/.test(s));
|
||||
});
|
||||
['deploy/nginx.conf', 'deploy/nginx-all.conf'].forEach(f => {
|
||||
// 接入层已移到宿主:四容器编排不再有 nginx 服务,模板随之改名为 *.example。
|
||||
['deploy/nginx.host.conf.example', 'deploy/nginx-all.conf'].forEach(f => {
|
||||
const s = read(path.join(ROOT, f)) || '';
|
||||
ok(f + ' 把 /admin/ 代理给 business', /location\s*\^~\s*\/admin\//.test(s) && /proxy_pass/.test(s));
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user