build+docs: 用 HostStack agent 源码实证校正部署事实,砍掉 dig 的 42.7MB 膨胀
从本机缓存的 registry.hoststack.dev/hoststack/agent 镜像里直接读出运行时 配置,纠正了此前靠猜的四条部署事实: hoststack.yaml - 撤回上一版把 install: 合并进 build.command 的"修正"——schema 里 install.command 本来就是合法键(agent 的 schema 与 yaml-parser.ts 均定义), install/build/start 是三条独立命令 - 补上为什么这个文件不能删:runtime: rust 的框架默认 start 命令是 ./target/release/app,与 Cargo 包名无关。删掉 YAML 后 Start Command 回落到 app,进程在绑定端口前就以 not found 退出 - 记录实测优先级:YAML 值生效,省略才回落框架默认值(Cargo.toml 只声明 [[bin]] message-weave,仓库里既无 app 目标也无 app.rs) Dockerfile - 移除 bind9-dnsutils:连带 libicudata.so.72(29.8MB)等多级依赖,实测 边际成本 42.7MB,是其余 7 个工具总和(19.7MB)的两倍多 - DNS 解析用基础镜像自带的 getent 已覆盖(getent hosts / ahostsv4 / ahostsv6) - 注释写明 19.7MB 的实测口径 docs/deployment.md - §5.1 重写:build 在 rust:slim-trixie、runner 是 debian:trixie-slim, 本仓库 Dockerfile 完全不被执行 - 修正"没有 install: 键"与"validate 拦未知键"两处错误说法:agent 部署路径 不应用 zod 校验,未知键被静默丢弃而非报错,validate 才是唯一防线 - §4.1 容器安全档改成源码实测值:readonlyRootfs/dropCapabilities/ noNewPrivileges 全为 true,pidsLimit 256,tmpfs 选项 rw,nosuid,nodev, size=64m——原先写的 noexec 并不存在;并标注 dev 档 rootfs 可写 - §8.1 明确两条部署路径的镜像差异(bookworm vs trixie) - §6 HEALTHCHECK 与二进制"内置 health 子命令"的过期说法 - 删除文档中指向不存在文件 app.rs 的引用 验证:GATE-DOCS PASS 6/6(PATH AUDIT 369 path-refs / 0); runtime 阶段实测构建成功且 curl/ps/pgrep/ss/ip/jq/nc/getent 齐备、 dig 按预期缺席、getent hosts 可解析、uid 999; 8 个工具 dpkg Installed-Size 合计 7935kB 而全量 du 差 62.3MB, 剔除 dig 后 19.7MB——这个差额就是本次砍掉的膨胀。
This commit is contained in:
parent
417f378d68
commit
978288f8d7
10
Dockerfile
10
Dockerfile
|
|
@ -11,11 +11,19 @@ FROM debian:bookworm-slim AS runtime
|
|||
# 诊断工具有意内置:容器 rootfs 只读,运行期无法 apt 安装,只能在构建期装入。
|
||||
# 全部为只读查询能力(本机探活 / 进程 / socket / DNS / JSON 解析),
|
||||
# 不引入包管理之外的写能力,也不增加 shell 解释器。
|
||||
# 合计实测 19.7 MB(用 du -sb 对全量文件系统与 debian:bookworm-slim 对比测得)。
|
||||
#
|
||||
# 刻意不装 bind9-dnsutils(dig):它连带 libicudata 等依赖要多花 42.7 MB,
|
||||
# 而 DNS 解析用基础镜像自带的 getent 就能覆盖:
|
||||
# getent hosts motofans.club -> A 记录
|
||||
# getent ahostsv4 <host> -> 仅 IPv4
|
||||
# getent ahostsv6 <host> -> 仅 IPv6
|
||||
# 需要 TLS 握手细节时改用 curl -sv,两者都已在上面。
|
||||
# 注意:不要在此加入 RUN_MODE —— 该标识符已随 Config::from_env() 删除,代码零读取。
|
||||
RUN apt-get update \
|
||||
&& apt-get install --no-install-recommends -y \
|
||||
ca-certificates tini \
|
||||
curl procps iproute2 jq bind9-dnsutils netcat-openbsd \
|
||||
curl procps iproute2 jq netcat-openbsd \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& useradd --system --create-home --home-dir /nonroot --shell /usr/sbin/nologin messageweave
|
||||
COPY --from=builder /build/target/release/message-weave /usr/local/bin/message-weave
|
||||
|
|
|
|||
|
|
@ -124,14 +124,32 @@
|
|||
|
||||
### 5.1 HostStack 原生 Rust 部署
|
||||
|
||||
HostStack 使用仓库根目录 `hoststack.yaml`:Rust runtime 先执行安装命令
|
||||
`cargo fetch --locked`,再执行构建命令 `cargo build --release --locked`,服务命令为
|
||||
`./target/release/message-weave`,监听单个
|
||||
HTTP 端口并以 `/healthz` 做 interval 30 秒、timeout 5 秒的健康检查。服务命令里**不带**
|
||||
`--debug`——远程联调开关只应临时加上(改法见 `hoststack.yaml` 的注释,双因子见 §2.1)。
|
||||
`REDIS_URL` 与
|
||||
HostStack 使用仓库根目录 `hoststack.yaml`:`runtime: rust` 的 agent 在
|
||||
`rust:slim-trixie` 里执行 `install.command`(`cargo fetch --locked`)与
|
||||
`build.command`(`cargo build --release --locked`),把产物拷进
|
||||
`debian:trixie-slim` 的 runner 容器,以 `start.command`(`./target/release/message-weave`)
|
||||
启动,监听单个 HTTP 端口,并以 `/healthz` 做 interval 30 秒、timeout 5 秒的健康检查。
|
||||
`install:` **是**合法的 schema 键——`install` / `build` / `start` 是三条独立命令,
|
||||
依赖获取不写进 `build.command`。服务命令里**不带** `--debug`——远程联调开关只应临时加上
|
||||
(改法见 `hoststack.yaml` 的注释,双因子见 §2.1)。`REDIS_URL` 与
|
||||
`CONFIG_ENCRYPTION_KEY` 必须配置为 HostStack Secret;不得将密钥值写入 YAML、镜像、日志
|
||||
或代码仓库。现有 Dockerfile 部署方式仍受支持。
|
||||
或代码仓库。**本仓库的 Dockerfile 不由该路径执行**——runner 镜像是 HostStack 自带的
|
||||
`debian:trixie-slim`,不是本文件的 `debian:bookworm-slim`,见 §3 与 §8.1 的说明。
|
||||
|
||||
**该 YAML 是运行时配置的真源,且不能删除**:YAML 声明的字段**覆盖**控制台已存的同名
|
||||
配置,YAML 省略的字段**回落到运行时框架默认值**——Rust 的默认值是 `cargo fetch` /
|
||||
`cargo build --release` / `./target/release/app`。最后那个默认值由 HostStack 自带、
|
||||
与本仓库包名无关:`Cargo.toml` 只声明一个 `[[bin]]`(名字 `message-weave`,
|
||||
路径 `src/main.rs`),仓库里既没有 `app` 这个二进制目标也没有对应源文件。删掉本文件后
|
||||
Start Command 回落为 `./target/release/app`,进程在绑定端口前就以 `not found` 退出。
|
||||
线上实测印证了此行为:控制台 UI 显示的是默认值 `./target/release/app`,而实际 PID 1 是
|
||||
`./target/release/message-weave`——说明在生效的是 YAML,不是控制台。
|
||||
|
||||
**校验是宽松的,不是拒绝式的**:agent 的部署路径不应用 zod schema 校验,
|
||||
`install` / `build` / `start` 等都是可选对象,**未知键会被静默丢弃而不是报错**,
|
||||
拼错一个键名(如 `healthcheck` 全小写)不会让部署失败,只会让对应配置悄悄失效。
|
||||
因此 `hoststack validate`(本地类型检查,不发起 API 调用)是唯一有效的防线,
|
||||
应放进 CI;本机无 Node 环境时至少保留一次人工 review。
|
||||
|
||||
全局业务开关通过受保护的 `GET|PUT /api/enabled` 管理,持久化 Redis key 为
|
||||
`config:enabled`,默认关闭且读取失败 fail-closed。关闭时 `/webhook/tg`、`/push/jmap`、
|
||||
|
|
@ -345,7 +363,7 @@ spec:
|
|||
- `GET /ready` → 端到端就绪探针(`ARCH-READY-BASELINE`):检查配置完整性 + Redis 可达性 + 出站只读探测(`GET {jmap_origin}/.well-known/jmap` 带 Basic 认证、`GET https://api.telegram.org/bot<token>/getMe`,各 3s 超时、**并行**(`tokio::join!`),最坏约 3s);四者全过 `200`(就绪报告 JSON 含真实 `jmap`/`telegram` 字段),任一失败 `503`(标准错误 envelope `{"error":"service_unavailable","request_id":<id>}` + `Retry-After: 30`)。因此**LB / ingress 的探针超时必须 > 3s(建议 ≥10s)**。Uptime Kuma 按状态码(期望 200)监控,不受响应体变化影响。
|
||||
- 两者均为**公开探针**(`SAF-PROBE-PUBLIC`):无鉴权、仅返回健康状态、**不含敏感信息**。
|
||||
- `/ready` 已对 JMAP session 与 Telegram getMe 做**真实出站探测**(3s/个),但不覆盖真实消息投递验收;编排与 Uptime Kuma 可用它做端到端就绪探测,注意其最坏约 3s,LB 探测超时需 > 3s(见上)。
|
||||
- Docker `HEALTHCHECK` 指向同一监听端口(app 内置 `health` 子命令或 wget 同端口),**不依赖独立端口**(`C-NO-TCP-EXPOSE`)
|
||||
- Docker `HEALTHCHECK` 指向同一监听端口(`wget`/`curl` 探同端口),**不依赖独立端口**(`C-NO-TCP-EXPOSE`)。二进制没有 `health` 子命令可指,见 §8.1 的说明
|
||||
- 仅保留通用平台映射(compose `HEALTHCHECK`、k8s probe);不写 Cloud Run/Fly 等特指内容(`NG-SERVERLESS-BIND`)
|
||||
|
||||
---
|
||||
|
|
@ -365,13 +383,24 @@ spec:
|
|||
|
||||
### 8.1 运行方式与就绪探测
|
||||
|
||||
> **本节的两条部署路径不要混淆**:上面的构建/运行命令是本仓库 **Dockerfile 路径**(本地 `docker run`
|
||||
> 或其他容器宿主机);线上 HostStack 部署走 §5.1 的 `hoststack.yaml` + `runtime: rust` agent,
|
||||
> 在 `rust:slim-trixie` 里构建、拷进 `debian:trixie-slim` 的 runner 容器运行,
|
||||
> **完全不执行本 Dockerfile**——两者连 Debian 版本都不同(bookworm vs trixie)。
|
||||
> 服务 argv 是 `./target/release/message-weave` 而不是镜像里的
|
||||
> `tini -- /usr/local/bin/message-weave`,因此镜像内的诊断工具与 `ENTRYPOINT` 在线上都不生效。
|
||||
> 判定当前跑的是哪条路径,看容器内 `/proc/1/cmdline` 与 `/etc/os-release`。
|
||||
|
||||
> **本仓库没有 `docker-compose.yml`。**此前本节给过一段 compose 片段,其中的 `healthcheck` 写成
|
||||
> `message-weave health --addr 127.0.0.1:8080` —— 这个子命令不存在(`src/main.rs` 无任何 CLI
|
||||
> 参数解析,二进制只起 axum 服务)。照抄那段示例必然得到:healthcheck 永久失败,或容器被判定
|
||||
> healthy 却什么都不做。示例已删除,改为下面的事实约定。
|
||||
>
|
||||
> 镜像现已内置 `curl`,因此用 `HEALTHCHECK` 探活在技术上可行;但本节仍建议由平台 ingress/探针
|
||||
> 探活——`/ready` 是唯一同时校验 redis/jmap/telegram 的信号,`HEALTHCHECK` 只能证明进程活着。
|
||||
> 镜像(`runtime` 阶段)现已内置 `curl`,因此在 **Dockerfile 部署路径**下用 `HEALTHCHECK` 探活在
|
||||
> 技术上可行;HostStack 官方文档也把 Dockerfile `HEALTHCHECK` 定为把新副本加入 Traefik 池前的
|
||||
> readiness 关卡。但 `runtime: rust` 的 HostStack agent 不走本 Dockerfile,该路径的 readiness 由
|
||||
> `hoststack.yaml` 的 `healthCheck.path` 决定,所以仍以平台探针为准——`/ready` 是唯一同时校验
|
||||
> redis/jmap/telegram 的信号,`HEALTHCHECK` 只能证明进程活着。
|
||||
|
||||
**运行**
|
||||
|
||||
|
|
@ -385,8 +414,23 @@ docker run --env-file .env -p 8080:8080 messageweave:latest
|
|||
- runtime 阶段为 `debian:bookworm-slim`,非 root(`useradd --system`,实测 uid 999,非 1000);
|
||||
无本地 volume、无数据库引擎(`C-NO-LOCAL-WRITE` / `C-NO-DB`)。
|
||||
- 镜像内置只读诊断工具:`curl`、`procps`(`ps`/`pgrep`/`free`)、`iproute2`(`ss`/`ip`)、`jq`、
|
||||
`bind9-dnsutils`(`dig`)、`netcat-openbsd`(`nc`)。容器 rootfs 只读,这些工具**只能在构建期装入**,
|
||||
`netcat-openbsd`(`nc`),合计实测 **19.7 MB**(全量文件系统 `du` 对比
|
||||
`debian:bookworm-slim`)。容器 rootfs 只读,这些工具**只能在构建期装入**,
|
||||
运行期无法 `apt` 安装——这也是它们写进 Dockerfile 而不是留给运维现场装的原因。
|
||||
刻意**不装** `bind9-dnsutils`(`dig`):它连带 `libicudata.so.72` 等多级依赖要多花
|
||||
**42.7 MB**,是其余 7 个工具总和的两倍多;DNS 解析用基础镜像自带的 `getent` 即可
|
||||
(`getent hosts <h>` 出 A 记录,`getent ahostsv4` / `getent ahostsv6` 分开取)。
|
||||
**但这条只对 Dockerfile 部署路径成立**:`runtime: rust` 的 agent 在
|
||||
`rust:slim-trixie` 里构建、把产物拷进 `debian:trixie-slim` 的 runner 容器运行,
|
||||
**完全不执行本 Dockerfile**,因此这些工具在 HostStack 服务里并不存在(线上实测
|
||||
`pgrep: not found`)。该路径下改用内核接口诊断:`/proc/1/cmdline`(实际 argv)、
|
||||
`/proc/1/environ`(实际 env)、`/proc/net/tcp`、`/proc/<pid>/fd`。
|
||||
HostStack 生产部署的容器安全档(由 agent 镜像内的部署执行器源码实测确认)为
|
||||
`readonlyRootfs: true` + `dropCapabilities: true` + `noNewPrivileges: true` +
|
||||
`pidsLimit: 256`;`/tmp` 与 `/var/tmp` 是 `rw,nosuid,nodev,size=64m` 的 tmpfs——
|
||||
注意**没有** `noexec`,但重启即清空,所以不要在那里放可执行文件或期望重启后仍在的文件。
|
||||
**dev 环境与此不同**:dev 档为 `readonlyRootfs: false`、`noNewPrivileges: false`,
|
||||
rootfs 可写,同一个二进制在 dev 与生产的可写行为可能不同。
|
||||
- Redis 由外部已认证实例提供,不与此服务同容器运行(`C-REDIS-EXTERNAL`)。
|
||||
|
||||
**就绪探测:用 `/ready`,不要用 `/healthz`**
|
||||
|
|
@ -398,8 +442,9 @@ docker run --env-file .env -p 8080:8080 messageweave:latest
|
|||
| `GET /api/status` | 缺必需环境变量时 503 + `{"status":"configuration-setup","missing":[...]}` | 排查"起来了但没干活" |
|
||||
|
||||
> **由平台 ingress 探测 `/ready`。**镜像现已内置 `curl`(见 Dockerfile `runtime` 阶段),
|
||||
> 所以容器内写 `HEALTHCHECK` 在技术上可行;但 `/healthz` 只能证明进程活着,
|
||||
> `/ready` 才同时校验 Redis 与上游,因此仍以平台探针为准。
|
||||
> 所以在 Dockerfile 部署路径下容器内写 `HEALTHCHECK` 在技术上可行(HostStack 的 `runtime: rust`
|
||||
> 路径不走该 Dockerfile,readiness 由 `hoststack.yaml` 的 `healthCheck.path` 决定);但
|
||||
> `/healthz` 只能证明进程活着,`/ready` 才同时校验 Redis 与上游,因此仍以平台探针为准。
|
||||
|
||||
**缺必需环境变量不会崩溃,但也不会干活。**缺 `REDIS_URL` 或 `CONFIG_ENCRYPTION_KEY` 时,进程
|
||||
降级为只读配置路由(内存态、空 token、`NoopWorker`),容器继续应答请求但不做任何业务。排查这类
|
||||
|
|
|
|||
|
|
@ -7,9 +7,18 @@
|
|||
# chat allowlist, LLM settings) is NOT read from environment variables at all.
|
||||
# It is written to Redis via /api/bootstrap or admin PUT on /api/config.
|
||||
#
|
||||
# Container rootfs is read-only, so diagnostic tools must be installed at image
|
||||
# build time (see the `runtime` stage of Dockerfile): curl, procps (ps/pgrep),
|
||||
# iproute2 (ss/ip), jq, bind9-dnsutils (dig), netcat-openbsd (nc).
|
||||
# Precedence (verified in the HostStack agent source, deploy-executor.ts):
|
||||
# a value declared here wins; a field omitted here falls back to the
|
||||
# per-runtime framework default. For runtime: rust those defaults are
|
||||
# `cargo fetch` / `cargo build --release` / `./target/release/app`.
|
||||
#
|
||||
# That last default is why start.command MUST be declared below: `app` is not
|
||||
# this package's binary name. Cargo.toml declares [[bin]] name = "message-weave"
|
||||
# with path = "src/main.rs"; there is no `app` target and no src/app.rs.
|
||||
# Deleting this file reverts the service to a start command that exits with
|
||||
# `./target/release/app: not found` before it can bind the port.
|
||||
#
|
||||
# Do NOT add --debug here for production. See the note above start.command.
|
||||
services:
|
||||
web:
|
||||
type: web_service
|
||||
|
|
@ -17,6 +26,7 @@ services:
|
|||
install:
|
||||
command: cargo fetch --locked
|
||||
build:
|
||||
# --locked on both steps: fail rather than rewriting Cargo.lock mid-deploy.
|
||||
command: cargo build --release --locked
|
||||
start:
|
||||
# Binary is named after the Cargo package: `message-weave`, not `app`.
|
||||
|
|
@ -24,10 +34,12 @@ services:
|
|||
command: ./target/release/message-weave
|
||||
# Temporary diagnosis only: change the line above to
|
||||
# command: ./target/release/message-weave --debug
|
||||
# and set the DEBUG_TOKEN secret. Then probe the debug surface from
|
||||
# inside the container, never through the public domain:
|
||||
# and set the DEBUG_TOKEN secret. The debug surface is gated by BOTH
|
||||
# factors — the argv flag AND a non-empty DEBUG_TOKEN — read once at
|
||||
# process start. Then probe it from inside the container, never through
|
||||
# the public domain:
|
||||
# curl -s -i -H "Authorization: Bearer <token>" http://127.0.0.1:8080/debug/config
|
||||
# Debug routes are gated by BOTH factors (see docs/deployment.md §2.1).
|
||||
# See docs/deployment.md §2.1 for the double-factor gate.
|
||||
healthCheck:
|
||||
# /healthz only, never /ready. /ready also probes redis, the JMAP session
|
||||
# and Telegram getMe, so a transient upstream blip would restart the
|
||||
|
|
|
|||
Loading…
Reference in New Issue