阅读提要

镜像大小的元凶几乎总是同一批:完整基础镜像、构建工具链、层叠加。搞懂分层机制,把 1.2GB 压到 120MB 只需要三步。

开始阅读

字号

先看镜像里到底有什么

docker history --no-trunc myapp:latest

这条命令按层列出每条指令产生的体积。绝大多数 1GB+ 的镜像,前几层就占了大半:node:20 完整版约 1.1GB,node:20-slim 约 240MB,node:20-alpine 约 130MB。

选错基础镜像,后面怎么优化都白搭。

镜像分层

分层机制:删文件不会让镜像变小

这是最容易误解的一点。

Dockerfile 的每条指令生成一层,层是只读的。RUN rm -rf /tmp/cache 会在新层里记录「这个文件被删了」,但上一层里的原始文件仍然存在于镜像中。

所以这种写法是无效的:

RUN npm install
RUN rm -rf node_modules/.cache   # 镜像体积不会减少

正确做法是把「产生」和「清理」放在同一条 RUN 里:

RUN npm ci && npm cache clean --force && rm -rf /root/.npm

同一条指令内的操作只生成一层,中间产物不会落盘。

第一步:多阶段构建

构建需要 TypeScript、构建工具、devDependencies;运行只需要产物和 production 依赖。多阶段把这两件事分开:

FROM node:20-alpine AS build
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/.output ./.output
COPY --from=build /app/package.json ./
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

最终镜像只包含运行产物,构建工具链全部留在上一阶段,不会被带入。

关键点:COPY --from=build 只复制明确指定的路径。如果写成 COPY --from=build /app /app,等于把 node_modules 和源码一起搬过来,多阶段就白做了。

第二步:把依赖装对

npm install 会装 devDependencies,npm ci --omit=dev 不会。如果运行时确实需要依赖(比如 better-sqlite3 这种带原生模块的),就在 runtime 阶段单独装一次 production 依赖:

COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile --prod

注意 pnpm 的 node_modules 默认是符号链接结构,某些场景下跨阶段复制会断开链接。要么在 runtime 阶段重新装,要么给 pnpm 加 --node-linker=hoisted。

第三步:层缓存要利用好

Docker 的层缓存规则是:某层指令或它依赖的文件变了,这一层及之后全部失效。

所以顺序很重要——把变化频率低的放前面:

COPY package.json pnpm-lock.yaml ./     # 依赖清单,很少变
RUN pnpm install                         # 只有清单变了才重跑
COPY . .                                 # 源码,经常变
RUN pnpm build

反过来写(先 COPY . . 再 install),任何一次代码改动都会导致依赖重装,构建时间从 20 秒变成 5 分钟。

配一个 .dockerignore 同样重要:

node_modules
.git
data
uploads
*.log

.git 目录经常有几个 GB,被 COPY . . 带进去,镜像直接胖一圈。

检查与守住

docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
dive myapp:latest        # 逐层查看,找出浪费

dive 会给出一个「浪费空间」评分,能直接指出哪一层有冗余文件。

体积不是一次优化就完事的。CI 里加一条检查,超过阈值就失败,比每隔半年人工体检有效得多。

常见误区清单

  1. 以为 rm 能减小体积——只能在同一层内有效;
  2. base 镜像用 latest——某天上游更新,构建突然失败或体积翻倍;
  3. 把 .env、.git、测试数据打进镜像——既有体积问题,也是安全事故;
  4. apt-get install 后没清 /var/lib/apt/lists——多几十 MB;
  5. 用 ADD 替代 COPY——ADD 会自动解压和下载,行为不可预期。

一句话

镜像体积 = 基础层 + 每层新增文件。控制住「基础层选小的」和「每层别留垃圾」,剩下来的都是细节。

你认为这篇文章怎么样?

还没有表情反馈

分享文章