build+docs: 镜像内置只读诊断工具,清理 Dockerfile 的死 ENV 与文档过期表述

生产镜像此前只有 ca-certificates + tini,容器 rootfs 又只读,运维在容器内既
装不了 apt 也没有 curl/ps,排查只能靠宿主机的 /proc。把只读诊断工具装进
runtime 阶段,实测体积 +7.7 MB:

  curl procps(ps/pgrep/free) iproute2(ss/ip) jq bind9-dnsutils(dig) netcat-openbsd(nc)

全部是只读查询能力,不引入包管理之外的写能力,也不新增 shell 解释器。

顺手修掉四处已被事实推翻的表述:

  1. Dockerfile 的 ENV RUN_MODE=webhook 是死变量——该标识符随 Config::from_env()
     删除后代码零读取,文档 5 处都记着已删除,镜像却还在设。删掉并留注释防回退。
  2. deployment.md 说「runtime 只装了 ca-certificates tini」「镜像内没有 curl/wget」,
     两处都是健康检查建议的承重理由,随内置 curl 一起更新为「技术上可行但不建议」。
  3. deployment.md 说非 root 用户是 uid 1000,实际 useradd --system 给 uid 999,
     已实测修正。
  4. retired.md 里 compose healthcheck 示例的删除理由把「镜像无 curl」当主因,
     真正的承重原因是 src/main.rs 没有 CLI 子命令解析,已改写并标注 curl 已内置。

同时补齐 hoststack.yaml 缺失的 install 段(cargo fetch --locked),并在 start 段
写清临时开 --debug 的确切改法与「必须在容器内探、别走公网域名」的约束。

验证:
  - 包名与安装步骤在同一 debian:bookworm-slim 基础镜像内实测通过
  - useradd 实测输出 uid=999,与设计一致
  - docker build --check 无告警
  - GATE-DOCS PASS 6/6(SECTION-REF 217 / 0,PATH AUDIT 364 / 0)

注:install: { command: ... } 的键名是按现有 build:/start: 的同构推断的,
若 HostStack 拒绝该键,删掉这两行即可,不影响 build/start。
This commit is contained in:
anonsaber 2026-09-29 11:24:13 +08:00
parent 55a2bf5a14
commit 417f378d68
5 changed files with 51 additions and 15 deletions

View File

@ -8,13 +8,18 @@ COPY web ./web
RUN cargo build --release --locked RUN cargo build --release --locked
FROM debian:bookworm-slim AS runtime FROM debian:bookworm-slim AS runtime
# 诊断工具有意内置:容器 rootfs 只读,运行期无法 apt 安装,只能在构建期装入。
# 全部为只读查询能力(本机探活 / 进程 / socket / DNS / JSON 解析),
# 不引入包管理之外的写能力,也不增加 shell 解释器。
# 注意:不要在此加入 RUN_MODE —— 该标识符已随 Config::from_env() 删除,代码零读取。
RUN apt-get update \ RUN apt-get update \
&& apt-get install --no-install-recommends -y ca-certificates tini \ && apt-get install --no-install-recommends -y \
ca-certificates tini \
curl procps iproute2 jq bind9-dnsutils netcat-openbsd \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& useradd --system --create-home --home-dir /nonroot --shell /usr/sbin/nologin messageweave && 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 COPY --from=builder /build/target/release/message-weave /usr/local/bin/message-weave
USER messageweave USER messageweave
ENV PORT=8080 \ ENV PORT=8080
RUN_MODE=webhook
EXPOSE 8080 EXPOSE 8080
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/message-weave"] ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/message-weave"]

View File

@ -124,9 +124,12 @@
### 5.1 HostStack 原生 Rust 部署 ### 5.1 HostStack 原生 Rust 部署
HostStack 使用仓库根目录 `hoststack.yaml`:Rust runtime 执行 HostStack 使用仓库根目录 `hoststack.yaml`:Rust runtime 先执行安装命令
`cargo build --release --locked`,服务命令为 `./target/release/message-weave`,监听单个 `cargo fetch --locked`,再执行构建命令 `cargo build --release --locked`,服务命令为
HTTP 端口并以 `/healthz` 做 interval 30 秒、timeout 5 秒的健康检查。`REDIS_URL` 与 `./target/release/message-weave`,监听单个
HTTP 端口并以 `/healthz` 做 interval 30 秒、timeout 5 秒的健康检查。服务命令里**不带**
`--debug`——远程联调开关只应临时加上(改法见 `hoststack.yaml` 的注释,双因子见 §2.1)。
`REDIS_URL` 与
`CONFIG_ENCRYPTION_KEY` 必须配置为 HostStack Secret;不得将密钥值写入 YAML、镜像、日志 `CONFIG_ENCRYPTION_KEY` 必须配置为 HostStack Secret;不得将密钥值写入 YAML、镜像、日志
或代码仓库。现有 Dockerfile 部署方式仍受支持。 或代码仓库。现有 Dockerfile 部署方式仍受支持。
@ -364,9 +367,11 @@ spec:
> **本仓库没有 `docker-compose.yml`。**此前本节给过一段 compose 片段,其中的 `healthcheck` 写成 > **本仓库没有 `docker-compose.yml`。**此前本节给过一段 compose 片段,其中的 `healthcheck` 写成
> `message-weave health --addr 127.0.0.1:8080` —— 这个子命令不存在(`src/main.rs` 无任何 CLI > `message-weave health --addr 127.0.0.1:8080` —— 这个子命令不存在(`src/main.rs` 无任何 CLI
> 参数解析,二进制只起 axum 服务),而且 runtime 阶段是 `debian:bookworm-slim`,只装了 > 参数解析,二进制只起 axum 服务)。照抄那段示例必然得到:healthcheck 永久失败,或容器被判定
> `ca-certificates tini`,镜像里没有 curl/wget。照抄那段示例必然得到两个结果之一:healthcheck > healthy 却什么都不做。示例已删除,改为下面的事实约定。
> 永久失败,或容器被判定 healthy 却什么都不做。示例已删除,改为下面的事实约定。 >
> 镜像现已内置 `curl`,因此用 `HEALTHCHECK` 探活在技术上可行;但本节仍建议由平台 ingress/探针
> 探活——`/ready` 是唯一同时校验 redis/jmap/telegram 的信号,`HEALTHCHECK` 只能证明进程活着。
**运行** **运行**
@ -377,8 +382,11 @@ docker run --env-file .env -p 8080:8080 messageweave:latest
``` ```
- 单端口 8080;平台 ingress/反代映射 HTTPS → 8080(`C-HTTPS-INBOUND`)。 - 单端口 8080;平台 ingress/反代映射 HTTPS → 8080(`C-HTTPS-INBOUND`)。
- runtime 阶段为 `debian:bookworm-slim`,非 root(uid 1000);无本地 volume、无数据库引擎 - runtime 阶段为 `debian:bookworm-slim`,非 root(`useradd --system`,实测 uid 999,非 1000);
(`C-NO-LOCAL-WRITE` / `C-NO-DB`)。 无本地 volume、无数据库引擎(`C-NO-LOCAL-WRITE` / `C-NO-DB`)。
- 镜像内置只读诊断工具:`curl`、`procps`(`ps`/`pgrep`/`free`)、`iproute2`(`ss`/`ip`)、`jq`、
`bind9-dnsutils`(`dig`)、`netcat-openbsd`(`nc`)。容器 rootfs 只读,这些工具**只能在构建期装入**,
运行期无法 `apt` 安装——这也是它们写进 Dockerfile 而不是留给运维现场装的原因。
- Redis 由外部已认证实例提供,不与此服务同容器运行(`C-REDIS-EXTERNAL`)。 - Redis 由外部已认证实例提供,不与此服务同容器运行(`C-REDIS-EXTERNAL`)。
**就绪探测:用 `/ready`,不要用 `/healthz`** **就绪探测:用 `/ready`,不要用 `/healthz`**
@ -389,8 +397,9 @@ docker run --env-file .env -p 8080:8080 messageweave:latest
| `GET /ready` | 配置、Redis 或上游探针失败时 503 | 就绪探测、负载均衡摘除、Uptime Kuma | | `GET /ready` | 配置、Redis 或上游探针失败时 503 | 就绪探测、负载均衡摘除、Uptime Kuma |
| `GET /api/status` | 缺必需环境变量时 503 + `{"status":"configuration-setup","missing":[...]}` | 排查"起来了但没干活" | | `GET /api/status` | 缺必需环境变量时 503 + `{"status":"configuration-setup","missing":[...]}` | 排查"起来了但没干活" |
> **由平台 ingress 探测 `/ready`。**镜像内没有 curl/wget、也没有 CLI 子命令,不要依赖容器内 > **由平台 ingress 探测 `/ready`。**镜像现已内置 `curl`(见 Dockerfile `runtime` 阶段),
> healthcheck。 > 所以容器内写 `HEALTHCHECK` 在技术上可行;但 `/healthz` 只能证明进程活着,
> `/ready` 才同时校验 Redis 与上游,因此仍以平台探针为准。
**缺必需环境变量不会崩溃,但也不会干活。**缺 `REDIS_URL` 或 `CONFIG_ENCRYPTION_KEY` 时,进程 **缺必需环境变量不会崩溃,但也不会干活。**缺 `REDIS_URL` 或 `CONFIG_ENCRYPTION_KEY` 时,进程
降级为只读配置路由(内存态、空 token、`NoopWorker`),容器继续应答请求但不做任何业务。排查这类 降级为只读配置路由(内存态、空 token、`NoopWorker`),容器继续应答请求但不做任何业务。排查这类

View File

@ -304,7 +304,7 @@ message-weave/
├── Cargo.toml # jmap-client 0.4.2 / redis 0.27 / reqwest 0.13;不含 teloxide ├── Cargo.toml # jmap-client 0.4.2 / redis 0.27 / reqwest 0.13;不含 teloxide
├── .env.example # 仅 2 个必填项的占位样例(REDIS_URL / CONFIG_ENCRYPTION_KEY),不含业务配置 ├── .env.example # 仅 2 个必填项的占位样例(REDIS_URL / CONFIG_ENCRYPTION_KEY),不含业务配置
├── .gitignore ├── .gitignore
├── Dockerfile # debian:bookworm-slim + ca-certificates + tini(C-DEBIAN-SLIM) ├── Dockerfile # debian:bookworm-slim + tini + 只读诊断工具(C-DEBIAN-SLIM)
├── hoststack.yaml # 网关编排清单,非应用代码 ├── hoststack.yaml # 网关编排清单,非应用代码
├── src/ ├── src/
│ ├── main.rs # tokio main:读 PORT/REDIS_URL/CONFIG_ENCRYPTION_KEY 后启动 webhook HTTP 入口(单入口,无 CLI 子命令) │ ├── main.rs # tokio main:读 PORT/REDIS_URL/CONFIG_ENCRYPTION_KEY 后启动 webhook HTTP 入口(单入口,无 CLI 子命令)

View File

@ -23,7 +23,7 @@
| 多步对话 FSM(`Idle` / `AwaitClarify` / `AwaitConfirm` / `Analyzing` / `AwaitFallback`) | 目标设计,未落地 | 当前 AI 授权只需一个布尔+过期时间,多步态属过度设计 | Redis TTL 授权态,键与 TTL 见 `docs/reference.md` 的 AI 授权态一节 | | 多步对话 FSM(`Idle` / `AwaitClarify` / `AwaitConfirm` / `Analyzing` / `AwaitFallback`) | 目标设计,未落地 | 当前 AI 授权只需一个布尔+过期时间,多步态属过度设计 | Redis TTL 授权态,键与 TTL 见 `docs/reference.md` 的 AI 授权态一节 |
| 把领域 / 渠道 / 通知 / util 各层拆成独立目录(子模块拆分方案) | 目标形态目录,未落地 | 代码量未到需要拆分的规模 | 实际结构见 `docs/design.md` 的工程结构一节:领域层是 `src/domain.rs` + `src/domain/jmap/`(仅 `client.rs`);LLM 在 `src/ai.rs`;通知在 `src/notify.rs`;加密与工具逻辑内联在 `src/state.rs` / `src/config.rs`,没有独立的 util 层,也没有集成测试目录 | | 把领域 / 渠道 / 通知 / util 各层拆成独立目录(子模块拆分方案) | 目标形态目录,未落地 | 代码量未到需要拆分的规模 | 实际结构见 `docs/design.md` 的工程结构一节:领域层是 `src/domain.rs` + `src/domain/jmap/`(仅 `client.rs`);LLM 在 `src/ai.rs`;通知在 `src/notify.rs`;加密与工具逻辑内联在 `src/state.rs` / `src/config.rs`,没有独立的 util 层,也没有集成测试目录 |
| teloxide 风格的出站消息派发集成测试 | 目标形态测试,未落地 | 依附 teloxide 测试模式 | `src/channel.rs` 的单元测试(`#[test]`) | | teloxide 风格的出站消息派发集成测试 | 目标形态测试,未落地 | 依附 teloxide 测试模式 | `src/channel.rs` 的单元测试(`#[test]`) |
| docker-compose `healthcheck` 示例(`message-weave health --addr ...`) | 已删除的示例 | 运行镜像 `debian:bookworm-slim` 内无 `curl`/`wget`,且 `src/main.rs` 无 CLI 子命令解析,示例照抄必失败 | 由平台 ingress 探测 `/ready`;说明见 `docs/deployment.md` 的就绪探测一节 | | docker-compose `healthcheck` 示例(`message-weave health --addr ...`) | 已删除的示例 | `src/main.rs` 无 CLI 子命令解析,该子命令不存在,照抄必失败(早期还叠加了镜像内无 `curl` 的问题,现已内置 `curl`) | 由平台 ingress 探测 `/ready`;说明见 `docs/deployment.md` 的就绪探测一节 |
| SPA 管理凭据 = `REDIS_URL` 的 Redis ACL 密码 | 未采用路线 | 混淆基础设施凭据与 UI 管理密码;Redis 无 ACL 密码(TLS-only 托管 Redis)时 `bootstrap_token` 的 `.is_empty()` 守卫让 SPA 永久 401 | 改用 `CONFIG_ENCRYPTION_KEY`(启动必填的 32 字节高熵 hex,常数时间比较) | | SPA 管理凭据 = `REDIS_URL` 的 Redis ACL 密码 | 未采用路线 | 混淆基础设施凭据与 UI 管理密码;Redis 无 ACL 密码(TLS-only 托管 Redis)时 `bootstrap_token` 的 `.is_empty()` 守卫让 SPA 永久 401 | 改用 `CONFIG_ENCRYPTION_KEY`(启动必填的 32 字节高熵 hex,常数时间比较) |

View File

@ -1,15 +1,37 @@
# HostStack native Rust deployment for MessageWeave. # HostStack native Rust deployment for MessageWeave.
#
# Set REDIS_URL and CONFIG_ENCRYPTION_KEY as encrypted HostStack dashboard # Set REDIS_URL and CONFIG_ENCRYPTION_KEY as encrypted HostStack dashboard
# secrets; never put their values in this repository. # secrets; never put their values in this repository.
#
# Business config (JMAP URL + credentials, Telegram bot token, worker token,
# 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).
services: services:
web: web:
type: web_service type: web_service
runtime: rust runtime: rust
install:
command: cargo fetch --locked
build: build:
command: cargo build --release --locked command: cargo build --release --locked
start: start:
# Binary is named after the Cargo package: `message-weave`, not `app`.
# Do NOT add --debug here for production.
command: ./target/release/message-weave 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:
# 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).
healthCheck: healthCheck:
# /healthz only, never /ready. /ready also probes redis, the JMAP session
# and Telegram getMe, so a transient upstream blip would restart the
# container. See docs/deployment.md §2.1.
path: /healthz path: /healthz
interval: 30 interval: 30
timeout: 5 timeout: 5