CI 机器每次拉镜像都要等几分钟,一看镜像体积:1.2GB。一个 Node 写的后端 API,凭什么比操作系统还大?瘦身后 180MB,记录一下做了什么。
第一步:看钱花哪儿了
docker history myapp:latest --format "{{.Size}}\t{{.CreatedBy}}" | head -20
一目了然:最大的两层是 npm install(含 devDependencies,500MB+)和基础镜像里的构建工具链。接下来对症下药。
第二步:多阶段构建
编译期需要的(devDependencies、gcc、源码),运行期一概不要:
# ---- 构建阶段 ----
FROM node:22-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---- 运行阶段 ----
FROM node:22-slim
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER node
CMD ["node", "dist/main.js"]
两个 FROM 之间的所有垃圾(缓存、dev 依赖、构建产物中间态)都留在 builder 层,最终镜像一克不带。
第三步:基础镜像与 .dockerignore
node:22(完整版)自带 gcc、git、man 手册,约 1GB;node:22-slim 约 200MB;纯静态产物(如前端 build 完的静态文件)用 nginx:alpine 或 distroless 更极致。同时检查 .dockerignore——没有它,.git、node_modules、日志全被打进上下文,既慢又大:
node_modules
.git
dist
*.log
.env
第四步:层缓存友好
把 package.json 的 COPY 和 npm ci 放在 COPY . . 之前——依赖不变时直接命中缓存层,日常构建从 4 分钟缩到 20 秒。这一步不减镜像体积,但减 CI 时间,幸福感一样强。
结果
| 阶段 | 体积 |
|---|---|
| 原始(node:22 + 全量依赖) | 1.2 GB |
| + 多阶段构建 | 410 MB |
| + slim 基础镜像 | 260 MB |
| + omit=dev / clean cache / dockerignore | 180 MB |
推送、拉取、冷启动全面提速。顺手把镜像扫了一遍 CVE,基础镜像小了,攻击面也跟着小。