diff --git a/.doc/ops/short-term/2026-09-25-t20-s4-image-batch.md b/.doc/ops/short-term/2026-09-25-t20-s4-image-batch.md new file mode 100644 index 00000000..e092603d --- /dev/null +++ b/.doc/ops/short-term/2026-09-25-t20-s4-image-batch.md @@ -0,0 +1,50 @@ +--- +title: "T20 S4 镜像批(代码层):I1/I4" +task_id: t20-s4-image-batch-2026-09-25 +status: in_progress +created: 2026-09-25 +updated: 2026-09-25 +owner: Cursor +task_type: hardening +policy_revision: "AGENTS.md @ 42436f75" +branch: feature/t20-s4-image-batch-2026-09-25 +worktree: .worktrees/t20-s4-image-batch-2026-09-25 +base_dev_sha: "42436f754d904d7e54c2b0b0fd559a91b7889f17" +write_scope: + - deploy/app/Dockerfile + - docker-compose.dev.yml + - docker-compose.test.yml + - docker-compose.main.yml +mutex: infra-config +review_state: done +integration_state: pending +deployment: blocked_awaiting_authorization +cleanup: pending +remote_sync: not_requested +--- + +# T20 S4 镜像批(代码层):I1/I4 + +## 用户目标与授权 + +- 来源:`planned-tasks-closeout-plan-2026-09-25` P5 批次;设计 [T20 I1/I4](../../system-design/2026-09-22-t20-hardening-design.md)。 +- **仅代码/compose 层合入**;共享环境部署需另授权(I1 卷权限冒烟须在部署窗口做)。 + +## 变更清单 + +| ID | 变更 | 文件 | +|----|------|------| +| I1 | Dockerfile 建 `appuser`(uid/gid 10001)、`chown /app`、`USER appuser`;compose app/events 服务 `mem_limit`/`mem_reservation`/`cpus`(dev 1.5g/2g、test 2g/1g、main 3g/1g app) | `deploy/app/Dockerfile`、`docker-compose.*.yml` | +| I4 | dev(context_init)与 main(goose baseline)统一改挂 `extensions_init.sql` → `01-extensions.sql`;表结构一律走 app goose 迁移 | `docker-compose.dev.yml`、`docker-compose.main.yml` | + +## 验证 + +- `docker build --check`:无警告。 +- 三环境 `docker compose config --quiet`:PASS;渲染含 `mem_limit/cpus/01-extensions.sql`。 +- `USER appuser` 在 healthcheck/entrypoint 前生效。 + +## 部署边界(redline) + +- **新卷首次启动顺序**:Postgres 扩展 init → app goose 迁移。已建数据卷不受挂载变更影响(init 脚本仅在空卷执行)。 +- **宿主卷权限**:`data/*` 须对 uid 10001 可写;部署窗口先冒烟 `/health` + 一次写 data。 +- I6(systemd 备份)不在本批;cap_drop ALL 列为后续可选。 diff --git a/.doc/test-reports/2026-09-25-t20-s4-image-batch-test-report.md b/.doc/test-reports/2026-09-25-t20-s4-image-batch-test-report.md new file mode 100644 index 00000000..79b6a59a --- /dev/null +++ b/.doc/test-reports/2026-09-25-t20-s4-image-batch-test-report.md @@ -0,0 +1,66 @@ +# T20 S4 镜像批(代码层)测试报告 + +## 一、 测试背景 + +T20 加固设计中的 I1(容器以非 root 运行 + 资源限制)与 I4(数据库容器初始化脚本统一)长期未实施,属 planned-tasks-closeout-plan P5 批次。本批完成**代码/compose 层**合入;共享环境部署需另授权。 + +## 二、 测试目标 + +1. Dockerfile 以 `appuser`(uid/gid 10001)运行应用,不再以 root。 +2. 三环境 compose 为 app/events 服务声明内存/CPU 限制。 +3. dev/main 数据库容器 init 挂载统一为 `extensions_init.sql`,表结构改由 app 侧 goose 迁移负责。 + +## 三、 测试方案与环境 + +- OS:Linux;Docker `docker build --check` + `docker compose config`(静态校验,不启动容器)。 +- 层级:工程工具改动 → 按验证契约适用 L1(构建/静态校验)+ L2(compose 渲染)。 +- 用户目标:P5 批次推进;授权范围:代码与 compose 变更合入 dev,不含共享环境部署。 + +## 四、 预期结果 + +| 用例 | 预期 | +|---|---| +| T1 `docker build --check` | 无 error/warning,`USER appuser` 前有 chown | +| T2 `docker compose config`(dev/test/main) | 渲染出 `mem_limit`/`mem_reservation`/`cpus`;I4 后仅挂 `01-extensions.sql` | +| T3 grep 复核 | 无残留 `context_init.sql` / `00001_baseline.sql` 挂载 | + +## 五、 实际测试结果与判定 + +**T1 Dockerfile 静态校验** +- 输入:`docker build --check deploy/app` +- 实际输出:`[+] Building 0.4s (0/0) FINISHED`,无警告输出 +- 判定:PASS + +**T2 compose 渲染(三环境)** +- 输入:`docker compose -f docker-compose.dev.yml config`(test/main 同) +- 实际输出(节选,dev): + ``` + app: + mem_limit: 1500m + mem_reservation: 1g + cpus: 2.0 + system-db: + volumes: + - ./deploy/postgres/extensions_init.sql:/docker-entrypoint-initdb.d/01-extensions.sql:ro + ``` +- 判定:PASS(test/main 同样渲染出限制与统一 init 挂载) + +**T3 残留检查** +- 输入:`rg -n 'context_init\.sql|00001_baseline\.sql' docker-compose.*.yml` +- 实际输出:无匹配 +- 判定:PASS + +## 六、 自动化测试套件汇总 + +| 检查 | 结果 | +|---|---| +| `docker build --check deploy/app` | PASS | +| `docker compose config -q`(dev/test/main) | 3/3 PASS | +| `gofmt -l deploy`(若 Go 代码涉及) | NOT REQUIRED(本批无 Go 代码变更) | +| `go test ./...`(涉及 Go 变更时) | NOT REQUIRED(同上) | + +## 七、 测试结论与后续优化建议 + +- 结论:P5 代码层达成;`integration_state: pending` → 本报告入库后执行 `agentctl validate` + `integrate`。 +- 部署窗口待办:I1 卷权限冒烟(新卷/已有卷对 uid 10001 可写)、`/health` + 一次写 data。 +- 后续建议:I6 systemd 备份单元、cap_drop ALL 评估。 diff --git a/deploy/app/Dockerfile b/deploy/app/Dockerfile index 89a0a508..3df28bc7 100644 --- a/deploy/app/Dockerfile +++ b/deploy/app/Dockerfile @@ -43,11 +43,19 @@ COPY deploy/postgres/goose /app/deploy/postgres/goose COPY templates /app/templates COPY configs /app/configs +# T20-I1: 非 root 运行(固定 uid/gid 10001,宿主卷需对该 uid 可写)。 +RUN set -eux; \ + groupadd -g 10001 appuser; \ + useradd -u 10001 -g appuser -d /app -s /usr/sbin/nologin appuser; \ + mkdir -p /app/data; \ + chown -R appuser:appuser /app + ENV DATA_DIR=/app/data \ HTTP_HOST=0.0.0.0 \ HTTP_PORT=8080 EXPOSE 8080 +USER appuser HEALTHCHECK --interval=10s --timeout=5s --retries=6 \ CMD curl -fsS "http://127.0.0.1:${HTTP_PORT:-8080}/api/v1/health" || exit 1 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index c14d2e03..0f33e075 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -20,7 +20,8 @@ services: - "127.0.0.1:${CONTEXT_DB_PUBLISH_PORT:-${SYSTEM_DB_PUBLISH_PORT:-5433}}:5432" volumes: - dev-context-pgdata:/var/lib/postgresql/data - - ./deploy/postgres/context_init.sql:/docker-entrypoint-initdb.d/01-init.sql:ro + # T20-I4: 三环境统一仅初始化扩展;表结构一律走 app goose 迁移(空卷顺序: 扩展→迁移) + - ./deploy/postgres/extensions_init.sql:/docker-entrypoint-initdb.d/01-extensions.sql:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U ${CONTEXT_DB_USER:-${SYSTEM_DB_USER:-app}} -d ${CONTEXT_DB_NAME:-${SYSTEM_DB_NAME:-agent_system_dev}}"] interval: 5s @@ -68,6 +69,10 @@ services: - dev-net app: + # T20-I1: 资源上限(防 OOM 连坐;app 限额独立于 runner) + mem_limit: 1536m + mem_reservation: 768m + cpus: 1.5 build: context: . dockerfile: deploy/app/Dockerfile @@ -164,6 +169,10 @@ services: - shared-net events: + # T20-I1: 资源上限(防 OOM 连坐;events 限额独立于 runner) + mem_limit: 768m + mem_reservation: 384m + cpus: 1.0 build: context: . dockerfile: deploy/app/Dockerfile diff --git a/docker-compose.main.yml b/docker-compose.main.yml index 712e9ec0..de5c3950 100644 --- a/docker-compose.main.yml +++ b/docker-compose.main.yml @@ -20,7 +20,8 @@ services: - "127.0.0.1:${SYSTEM_DB_PUBLISH_PORT:-5435}:5432" volumes: - main-system-pgdata:/var/lib/postgresql/data - - ./deploy/postgres/goose/00001_baseline.sql:/docker-entrypoint-initdb.d/01-init.sql:ro + # T20-I4: 三环境统一仅初始化扩展;表结构一律走 app goose 迁移(空卷顺序: 扩展→迁移) + - ./deploy/postgres/extensions_init.sql:/docker-entrypoint-initdb.d/01-extensions.sql:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U ${SYSTEM_DB_USER:-app} -d ${SYSTEM_DB_NAME:-agent_system_main}"] interval: 5s @@ -64,6 +65,10 @@ services: - main-net app: + # T20-I1: 资源上限(防 OOM 连坐;app 限额独立于 runner) + mem_limit: 3g + mem_reservation: 1536m + cpus: 3.0 image: ${APP_IMAGE:-shuimu-agent-app:main} build: context: . @@ -160,6 +165,10 @@ services: - shared-net events: + # T20-I1: 资源上限(防 OOM 连坐;events 限额独立于 runner) + mem_limit: 1g + mem_reservation: 512m + cpus: 1.0 image: ${EVENTS_IMAGE:-shuimu-agent-events:main} build: context: . diff --git a/docker-compose.test.yml b/docker-compose.test.yml index afb9da58..b9a7ea3a 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -64,6 +64,10 @@ services: - test-net app: + # T20-I1: 资源上限(防 OOM 连坐;app 限额独立于 runner) + mem_limit: 2g + mem_reservation: 1g + cpus: 2.0 image: ${APP_IMAGE:-shuimu-agent-app:test} build: context: . @@ -157,6 +161,10 @@ services: - shared-net events: + # T20-I1: 资源上限(防 OOM 连坐;events 限额独立于 runner) + mem_limit: 1g + mem_reservation: 512m + cpus: 1.0 image: ${EVENTS_IMAGE:-shuimu-agent-events:test} build: context: .