返回 AICAP-180
B8 · Day 78真 API + 流式 + Docker

镜像瘦身 <300MB

B8 这一段把服务封装成镜像。

阶段: B8 · 真 API + 流式 + Docker(Day 71-80) 标签: #docker #image-slimming #standalone #dockerignore

今日导引(由浅入深)

B8 这一段把服务封装成镜像。 昨天(Day 77)用多阶段 Dockerfile.mcp 跑出首版体积,但 runtime 层还从 deps 阶段搬了整套含 devDeps 的 node_modules——偏胖。

今天做瘦身工程:把几种正交手段叠起来——依赖裁剪、--prod 安装、Next standalone 输出、严格 .dockerignore—— 目标压到 <300MB 并仍能 docker run 跑通流式。

这一步直接服务于明天(Day 80)的端到端固化与 B9 的云部署: 镜像越小,冷启动越快、拉取越快、攻击面越小。 今天的「最小可判定产出」是 docker images 显示 <300MB + 一段 docker run 跑通 /chat(或 mcp:serve)流式的 transcript。

1. 机理精读

瘦身是多种正交手段的叠加,不是单点优化。 没有银弹,靠累乘。四种手段:

  1. 依赖裁剪(只装 runtime deps)pnpm install --prod / --production 只装 dependencies,不装 devDependencies。 编译期工具(tsc、eslint、vitest、类型定义)全部不进 runtime——这往往是最大单笔节省。
  2. Next.js standalone 输出(output: 'standalone': Next 的 Output File Tracing 静态分析「实际被 import 的模块」,只把这些 + 一个最小 server 打进 .next/standalone。 它能把 node_modules 从「装了什么」缩到「真用了什么」,常见从数百 MB 砍到几十 MB。
  3. 严格 .dockerignore: 排除 node_modules(本地的不该进上下文,应在镜像内重装)、.gitdocscoverage.next、测试与日志。 它既缩小构建上下文(build 更快),又防止本地大目录误打进镜像。
  4. distroless runtime 层: 再砍掉 shell / 包管理器的体积与攻击面(见 Day 77)。

为什么定 <300MB 这个数? 它是个合理工程目标,不是硬性 SOTA。 Node 场景用 standalone + distroless 通常能落到这个量级; 定一个具体阈值是为了让「瘦身做没做到位」可判定,而非追求极限最小。 对低 / 突发流量的 eval runner,小镜像意味着 Cloud Run 冷启动更快(拉镜像是冷启动延迟的一部分,明天 B9 会量化)。

关键权衡:可调试性 vs 体积 / 安全。

  • distroless 没 shell,docker exec 进不去——出问题不能 exec 进容器手动戳。
  • standalone 输出剥掉了未被静态分析识别到的动态 require(极少数运行时 require(variable) 会漏)。

瘦身越激进,越要靠完整的容器内 eval 回归(Day 80)来兜底证明「真能跑」,而非靠手动 exec 进容器戳。 这是「越小越要测」的辩证:体积换来了部署效率,可信度就转嫁给了自动化回归。

与相邻概念的边界。 Day 77 解决「分层 + 别装编译垃圾」;今天解决「runtime 层本身还能再瘦多少」。 最常见的膨胀源是「build 缓存 / devDeps 漏进 runtime 层」——瘦身的第一刀永远先砍这个。

2. 推导 / 手算 / 代码走读

代码走读 .dockerignore(仓库已有)——逐项确认排除完整:

  1. node_modules:排除本地依赖目录(镜像内 pnpm install --frozen-lockfile 重装,保证平台一致)。
  2. .next / out / build:排除本地构建产物(容器内重新构建)。
  3. .git:排除版本历史(可观体积 + 不该进镜像)。
  4. docs:排除文档(含本批笔记,runtime 无关)。
  5. agent-evals/reports:排除已生成的 eval 报告(运行时产物,不该烤进镜像)。
  6. .env / .env*.local排除密钥文件——防止把本地 key 误打进镜像层(安全关键)。
  7. *.log / coverage:排除日志与覆盖率报告。

走读结论:.dockerignore 已覆盖 node_modules / 构建产物 / .git / docs / 报告 / env / 日志这几大类,排除项完整。 瘦身的下一刀在 Dockerfile.mcp 的 runtime 层—— 当前它 COPY --from=deps 搬的是含 devDeps 的整套 node_modules,优化方向是改成 pnpm install --prod 或单独 prod-deps 阶段。

诚实标注:本仓 MCP server 当前用 tsx 直跑 TS(见 CMD ["pnpm","mcp:serve"]),并非 Next standalone 路径; output:'standalone' 是 Next 应用侧的瘦身手段,本日作为通用方法论记录, MCP 镜像的瘦身主刀是 prod-deps 裁剪 + distroless。

简单估算瘦身上限:一个典型 Node 项目 node_modules 里 devDeps(类型定义 + 测试框架 + 编译器)常占 60-70%, 单是 --prod 一刀就能把依赖体积砍掉一半以上——这也是为什么「devDeps 进 runtime」是首要膨胀源。

瘦身效果的验证顺序也有讲究,不能只看 docker images 那一个总数:

  • 先用 docker history <image> 看每层增量,定位最大的那几层(通常是 node_modules 那层)。
  • 再决定该层是用 --prod 裁、用 standalone 重打,还是换 distroless runtime。
  • 最后 docker images 看总数是否落到 <300MB——总数只是结果,分层增量才指出该砍哪刀。

这套「先归因再瘦身」的顺序,避免了盲目叠 trick 却不知道哪一刀真正生效。

3. 今日实战

  1. Dockerfile.mcp 基础上优化 runtime 层: 把搬整套 node_modules 改为 prod-only(pnpm install --prod --frozen-lockfile 或独立 prod-deps 阶段),可叠 distroless runtime。
  2. docker build -f Dockerfile.mcp -t momoweb3-mcp . 重建。
  3. docker run -p 8765:8765 momoweb3-mcp 起容器,跑通 /chat(或 mcp:serve)流式响应。
  4. docker images 量最终体积,核对 <300MB;截图 + 抓流式响应 transcript 入 worklog。
  5. 复核 .dockerignore 排除项完整(上节已确认就绪),特别是 .env 一类密钥不外泄。

4. 今日实测 / 产出

  • 外部动作 / 手动 docker step——镜像 <300MB 截图 + 流式响应 transcript 需本机执行后采集。
  • 仓库侧前置件(Dockerfile.mcp.dockerignore已就绪;体积数字与 run transcript 为待采集的 checkable 产出
  • 诚实标注:尚未在本笔记里写死任何已测体积数字,因为它要本机跑过才有;不臆造 MB 数。

5. 常见误区 / 陷阱

  1. build 缓存 / devDeps 漏进 runtime 层——最常见的膨胀源。瘦身第一刀必先砍它(prod-only 安装)。
  2. .dockerignore 没排 .env: 把本地密钥烤进镜像层,推到 registry 即泄密。本仓 .dockerignore 已含 .env / .env*.local,但每次新增 env 文件名都要复核。
  3. 瘦身后只手动 exec 验证: distroless 没 shell 根本进不去,且单点戳不等于回归通过。必须用容器内完整 eval(Day 80)证明真能跑。
  4. 把 <300MB 当硬性 SOTA 死磕: 它是合理工程目标;为多砍 20MB 牺牲可维护性不值。

6. 学习资源(每条带 YYYY-MM)

  • Next.js — Output File Tracing / output: 'standalone' 文档(Vercel, 2026-01)。
  • pnpm — install --prod / --frozen-lockfile 文档(pnpm, 2025-12)。
  • Docker — .dockerignore 与构建上下文文档(Docker, 2025-11)。
  • Distroless nodejs 镜像(GoogleContainerTools, 2025-10)。
  • 本仓 artifact:Dockerfile.mcp.dockerignore(2026-06,AICAP-180 B8)。

SOTA检查 (2026-06 更新)

  • Next standalone + distroless 组合通常可达 <300MB(Node 场景);<300MB 为合理工程目标而非硬性 SOTA
  • 过时黑名单:把 build 缓存 / devDeps 漏进 runtime 层(最常见膨胀源);.dockerignore 漏排 .envnode:latest 全量基镜。
  • 下次复查点:Day 80 在容器内重跑 29 任务回归,证明瘦身后「真能跑」;distroless / Next 版本执行当周核对。

衔接

  • 昨天:Day 77 — 多阶段 Docker(首版体积基线,runtime 仍含 devDeps)
  • 今天:叠 prod-only / standalone / 严格 .dockerignore 把镜像压到 <300MB 并跑通流式——体积与 run transcript 待本机采集
  • 明天:Day 79 — vibe-coding worklog(把这一路的真实数字写成公开 worklog 作品集资产)