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

1404 字
7 分钟
09进阶:构建进阶与镜像瘦身

第 9 关 构建进阶与镜像瘦身#

本关的产出:让重复构建稳定命中缓存、明显变快,并把构建工具从最终镜像里清出去。

9.1 第 3 关留下的下半场:构建也能复用#

第 3 关讲分层时说过一句没展开的话:层能被复用,构建的时候也一样。

docker build 会逐条读 Dockerfile,给每条指令算一个「指纹」:RUN 的指纹是那条命令本身,COPY 的指纹是复制进去的文件内容,FROM 的指纹是基础镜像。

指纹和上次一样 → 这一层直接拿上次的,不重跑,输出里打 CACHED。不一样 → 重做,并且从这里往下,每一步都不再复用(后面都建在它上面)。

(逐条看结果:docker build 的输出就是一行一条。)

9.2 顺序决定命中:把不常变的放前面#

拿一个 Node 应用举例:

FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

能跑,但改一行源码就触发 COPY . . 失效 → 它后面每一步全部重来,包括最慢的 npm install。源码天天改,依赖几周才动一次,这个顺序把最贵的活儿排在了最容易失效的地方。

换个顺序:

FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["node", "server.js"]

现在依赖清单单独先复制。改源码 → COPY package*.json 没变、npm install 那层命中 CACHED,只有最后的 COPY . . 重做,秒级完成。改依赖 → 从 COPY package*.json 起重做,该跑的照跑。

规矩一句话:不会变的放前面,会变的放后面。

易踩的坑:某一层的指纹一变,它后面所有层都不再复用——不是只有那一条重做。所以最贵的指令(装依赖、编译)要尽量排在前面、排在很少变的输入后面。

9.3 多阶段构建:把构建工具挡在门外#

9.2 那份 Dockerfile 还藏着更大的浪费:它把整个 node 装进了最终镜像。可跑起来的应用只要那几份产物,编译它的那套工具根本不该上生产。

多阶段构建就是干这个的:

# 第一段:编译
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# 第二段:成品
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html

一份 Dockerfile,两个 FROM,就是两个阶段:

  1. build 阶段(AS build 给它起名)装依赖、编译,产出一堆构建垃圾。
  2. 成品阶段从 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 mysql
VOLUME /var/lib/mysql

一行,两个后果。

后果一:docker history 里它是一条 0B。 回扣第 3 关那句——只改配置、不碰文件系统的指令不占层。VOLUME 就是这一类:它登记了一个「这个位置归卷管」的声明,没往文件系统里写任何东西。

后果二:docker run 时自动生成一个匿名卷。 这正是第 4 关那些「看不出用途、也不敢删」的僵尸卷的来源之一——你一个 -v 都没写,卷照样冒出来。

VOLUME 是镜像作者的东西。写自己的镜像时,除非真有这个需要(数据必须外置),别随手加——每起一个容器就多一个匿名卷。

9.5 还能再瘦的地方#

几条通用的:

  1. 换个更小的底:同一个 nginx,nginx 建在 Debian 上,nginx:alpine 建在 Alpine 上——Alpine 是个精简得多的 Linux,所以后者小一大截。选底的时候挑刚好够用的那个。
  2. 装完就清,而且要清在同一条 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 标签的坑。

本关命令速查#

构建与镜像,见 命令速查表 · 构建镜像。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
09进阶:构建进阶与镜像瘦身
https://hecloud.top/posts/tech/docker/09构建进阶与镜像瘦身/
作者
贺小云
发布于
2026-10-12
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
贺小云
热爱技术与折腾的博客 Dancing with uncertainty. 与不确定性共舞
公告
出于成本考虑,贺云已全面转型Serverless架构
分类
标签
碎碎念
站点统计
文章
91
分类
5
标签
44
总字数
103,192
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
Cloudflare Pages
博客版本
Firefly v6.16.5
文章许可
CC BY-NC-SA 4.0