09进阶:构建进阶与镜像瘦身

第 9 关 构建进阶与镜像瘦身
本关的产出:让重复构建稳定命中缓存、明显变快,并把构建工具从最终镜像里清出去。
9.1 第 3 关留下的下半场:构建也能复用
第 3 关讲分层时说过一句没展开的话:层能被复用,构建的时候也一样。
docker build 会逐条读 Dockerfile,给每条指令算一个「指纹」:RUN 的指纹是那条命令本身,COPY 的指纹是复制进去的文件内容,FROM 的指纹是基础镜像。
指纹和上次一样 → 这一层直接拿上次的,不重跑,输出里打 CACHED。不一样 → 重做,并且从这里往下,每一步都不再复用(后面都建在它上面)。
(逐条看结果:docker build 的输出就是一行一条。)
9.2 顺序决定命中:把不常变的放前面
拿一个 Node 应用举例:
FROM node:20WORKDIR /appCOPY . .RUN npm installCMD ["node", "server.js"]能跑,但改一行源码就触发 COPY . . 失效 → 它后面每一步全部重来,包括最慢的 npm install。源码天天改,依赖几周才动一次,这个顺序把最贵的活儿排在了最容易失效的地方。
换个顺序:
FROM node:20WORKDIR /appCOPY package*.json ./RUN npm installCOPY . .CMD ["node", "server.js"]现在依赖清单单独先复制。改源码 → COPY package*.json 没变、npm install 那层命中 CACHED,只有最后的 COPY . . 重做,秒级完成。改依赖 → 从 COPY package*.json 起重做,该跑的照跑。
规矩一句话:不会变的放前面,会变的放后面。
易踩的坑:某一层的指纹一变,它后面所有层都不再复用——不是只有那一条重做。所以最贵的指令(装依赖、编译)要尽量排在前面、排在很少变的输入后面。
9.3 多阶段构建:把构建工具挡在门外
9.2 那份 Dockerfile 还藏着更大的浪费:它把整个 node 装进了最终镜像。可跑起来的应用只要那几份产物,编译它的那套工具根本不该上生产。
多阶段构建就是干这个的:
# 第一段:编译FROM node:20 AS buildWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build
# 第二段:成品FROM nginx:alpineCOPY --from=build /app/dist /usr/share/nginx/html一份 Dockerfile,两个 FROM,就是两个阶段:
build阶段(AS build给它起名)装依赖、编译,产出一堆构建垃圾。- 成品阶段从
nginx:alpine重新开始——干干净净,上一阶段的 node 不带过来,只把dist里那点成果拷进来。
关键在 COPY --from=build /app/dist /usr/share/nginx/html 这一行。它的第一个参数从哪儿算,看有没有 --from:
- 没有
--from(普通COPY):从docker build末尾那个.交出去的目录里找; - 有
--from=build:改从build阶段的文件系统里取。
上一阶段用完就扔,不进最终镜像。
结果:最终镜像 = nginx:alpine 加一份静态文件,node 那一大坨(几百 MB)留在门外。这就是「1GB → 100MB」的来源。
易踩的坑:--from= 后面既可以写阶段名(build),也可以写镜像名(--from=nginx:latest),用来从任意现成镜像里捞文件。
9.4 VOLUME:第 4 关那条没点名的指令
第 4 关讲匿名卷时说「有些镜像在构建的时候就声明了某个目录是卷」,当时没给指令名(那是写 Dockerfile 的活)。它叫 VOLUME:
FROM mysqlVOLUME /var/lib/mysql一行,两个后果。
后果一:docker history 里它是一条 0B。 回扣第 3 关那句——只改配置、不碰文件系统的指令不占层。VOLUME 就是这一类:它登记了一个「这个位置归卷管」的声明,没往文件系统里写任何东西。
后果二:docker run 时自动生成一个匿名卷。 这正是第 4 关那些「看不出用途、也不敢删」的僵尸卷的来源之一——你一个 -v 都没写,卷照样冒出来。
VOLUME 是镜像作者的东西。写自己的镜像时,除非真有这个需要(数据必须外置),别随手加——每起一个容器就多一个匿名卷。
9.5 还能再瘦的地方
几条通用的:
- 换个更小的底:同一个 nginx,
nginx建在 Debian 上,nginx:alpine建在 Alpine 上——Alpine 是个精简得多的 Linux,所以后者小一大截。选底的时候挑刚好够用的那个。 - 装完就清,而且要清在同一条
RUN里:
RUN apt-get update && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/*这条要讲清:一条 RUN 就是一层,按写的先后从上往下排——所以装的那条落在第 1 层,它留下的索引实物就搁在第 1 层里。拆成两条时,删的那条落在第 2 层,只能在新一层盖一个「已删」的标记,盖不住实物,镜像一点没小。清在同一条 RUN 里,则是这层定稿前就把索引擦掉,快照里压根没有它。
(层为什么擦不掉下层的实物,是 overlayfs 的事——层是「差异」、删除只是个「标记」,机制归 第 15 关 · 底层原理。)
3. .dockerignore(第 3 关讲过)挡掉 .git、node_modules 这类根本不该进去的东西。
这一关的产出:会让构建命中缓存、知道怎么把镜像瘦下来,也认识了 VOLUME 这条会悄悄造卷的指令。下一关把造好的镜像推出去——以及那个 latest 标签的坑。
本关命令速查
构建与镜像,见 命令速查表 · 构建镜像。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!





