镜像瘦身 <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. 机理精读
瘦身是多种正交手段的叠加,不是单点优化。 没有银弹,靠累乘。四种手段:
- 依赖裁剪(只装 runtime deps):
pnpm install --prod/--production只装dependencies,不装devDependencies。 编译期工具(tsc、eslint、vitest、类型定义)全部不进 runtime——这往往是最大单笔节省。 - Next.js standalone 输出(
output: 'standalone'): Next 的 Output File Tracing 静态分析「实际被 import 的模块」,只把这些 + 一个最小 server 打进.next/standalone。 它能把node_modules从「装了什么」缩到「真用了什么」,常见从数百 MB 砍到几十 MB。 - 严格
.dockerignore: 排除node_modules(本地的不该进上下文,应在镜像内重装)、.git、docs、coverage、.next、测试与日志。 它既缩小构建上下文(build 更快),又防止本地大目录误打进镜像。 - 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(仓库已有)——逐项确认排除完整:
node_modules:排除本地依赖目录(镜像内pnpm install --frozen-lockfile重装,保证平台一致)。.next/out/build:排除本地构建产物(容器内重新构建)。.git:排除版本历史(可观体积 + 不该进镜像)。docs:排除文档(含本批笔记,runtime 无关)。agent-evals/reports:排除已生成的 eval 报告(运行时产物,不该烤进镜像)。.env/.env*.local:排除密钥文件——防止把本地 key 误打进镜像层(安全关键)。*.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. 今日实战
- 在
Dockerfile.mcp基础上优化 runtime 层: 把搬整套node_modules改为 prod-only(pnpm install --prod --frozen-lockfile或独立 prod-deps 阶段),可叠 distroless runtime。 docker build -f Dockerfile.mcp -t momoweb3-mcp .重建。docker run -p 8765:8765 momoweb3-mcp起容器,跑通/chat(或mcp:serve)流式响应。docker images量最终体积,核对 <300MB;截图 + 抓流式响应 transcript 入 worklog。- 复核
.dockerignore排除项完整(上节已确认就绪),特别是.env一类密钥不外泄。
4. 今日实测 / 产出
- 外部动作 / 手动 docker step——镜像 <300MB 截图 + 流式响应 transcript 需本机执行后采集。
- 仓库侧前置件(
Dockerfile.mcp、.dockerignore)已就绪;体积数字与 run transcript 为待采集的 checkable 产出。 - 诚实标注:尚未在本笔记里写死任何已测体积数字,因为它要本机跑过才有;不臆造 MB 数。
5. 常见误区 / 陷阱
- build 缓存 / devDeps 漏进 runtime 层——最常见的膨胀源。瘦身第一刀必先砍它(prod-only 安装)。
.dockerignore没排.env: 把本地密钥烤进镜像层,推到 registry 即泄密。本仓.dockerignore已含.env/.env*.local,但每次新增 env 文件名都要复核。- 瘦身后只手动 exec 验证: distroless 没 shell 根本进不去,且单点戳不等于回归通过。必须用容器内完整 eval(Day 80)证明真能跑。
- 把 <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漏排.env;node:latest全量基镜。 - 下次复查点:Day 80 在容器内重跑 29 任务回归,证明瘦身后「真能跑」;distroless / Next 版本执行当周核对。
衔接
- 昨天:Day 77 — 多阶段 Docker(首版体积基线,runtime 仍含 devDeps)
- 今天:叠 prod-only / standalone / 严格 .dockerignore 把镜像压到 <300MB 并跑通流式——体积与 run transcript 待本机采集
- 明天:Day 79 — vibe-coding worklog(把这一路的真实数字写成公开 worklog 作品集资产)