2.2 Dockerfile指令与构建过程


2.2 Dockerfile 指令与构建过程

摘要:Dockerfile 是制造层的流水线:每条改文件系统的指令产出一层,构建缓存按指令与输入逐层匹配。本节逐条拆解常用指令的真实行为,用可复现实验建立缓存命中的直觉,并指出那些"看起来等价、实际上天差地别"的写法。

学习目标

  1. 准确说出 FROM、RUN、COPY、ENV、CMD、ENTRYPOINT 各自的层行为
  2. 用实验验证"指令不变则缓存命中,输入变了则其后全部失效"
  3. 分清 CMD 与 ENTRYPOINT 的组合语义
  4. 写出缓存友好的指令排序

一条指令就是一层

从一份最小的 Dockerfile 开始:

FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . ENV TZ=Asia/Shanghai CMD ["python", "server.py"]

执行构建:

docker build -t demo:1 .

输出里每一行 Step 就是流水线的一道工序。关键观察是第二次构建:

docker build -t demo:1 .

这次几乎每一步都秒过,输出里满是 CACHED。现在改一下业务代码再构建——只改 server.py,不动 requirements.txt:

docker build -t demo:2 .

输出出现了分水岭:COPY requirements.txt . 之前全部 CACHED,COPY . . 开始重新执行,pip install 也在缓存里活着。这就是缓存的核心规则:缓存按层匹配,从第一条变化的指令起,其后所有层全部重建。而 COPY requirements.txtCOPY . . 被拆成两步,正是为了让代价高昂的 pip install 依赖一份几乎不变的小文件,从而躲过业务代码变更引发的缓存雪崩。

常用指令的真实行为

FROM:指定基础镜像,是第零层。选基础镜像是镜像体积与构建复杂度的第一道分水岭,slim 与 alpine 变体的差别在 2.3 节展开。

RUN:在容器里执行一条命令并把文件系统差异定格为一层。两条 RUN 各成一层:

RUN apt-get update RUN apt-get install -y curl

这个写法有个隐蔽的问题:apt 缓存层在第一条里生成,装包在第二条里用。如果第二条缓存失效重跑,而第一条还是旧缓存,装的就是旧索引里的包。更糟的是两条 RUN 各留下一份中间垃圾。合并成一条是标准做法:

RUN apt-get update && apt-get install -y curl \ && rm -rf /var/lib/apt/lists/*

同一层里装完即清理,垃圾不进层。

COPY 与 ADD:把文件复制进镜像并成层。ADD 多了自动解压远端压缩包与 URL 下载两个魔法,恰恰因为魔法不可预期,社区共识是一律用 COPY,除非你明确需要解压功能。COPY 还带缓存细节:它按文件内容校验缓存,文件内容没变就命中,哪怕时间戳变了。

ENV、WORKDIR、EXPOSE、LABEL:只写元数据,不产生文件层。它们照样参与缓存链——改了 ENV,其后的 RUN 层全部失效,因为指令内容变了。

CMD 与 ENTRYPOINT:都不产生层,都是给容器定默认入口,区别在运行时如何拼接。Docker 实际执行的命令是 ENTRYPOINT + CMD 的拼接:

ENTRYPOINT ["python", "server.py"] CMD ["--port", "8000"]

不带参数启动,进程就是 python server.py --port 8000docker run demo --port 9000 时 CMD 被整体替换,变成 9000。这就是它们的标准分工:ENTRYPOINT 定"程序",CMD 定"默认参数"。exec 数组形式(JSON 数组)与 shell 形式的差别也有讲究:CMD python server.py 实际执行 /bin/sh -c "python server.py",你的程序成了 sh 的子进程,接收不到 SIGTERM 信号,容器要等 10 秒超时才被强杀。生产镜像一律用 exec 数组形式。

HEALTHCHECK:给容器一个自描述的健康检查,编排系统按它判断容器是否需要重启,第 6 章会用到。

构建缓存的匹配规则

把缓存规则整理成一张表:

变化源 缓存影响
指令文本变化 该层失效,其后全部失效
COPY 的源文件内容变化 该层失效,其后全部失效
基础镜像更新(同 tag) 全部失效
只是重新 build 全部命中

由此推出缓存友好的指令排序原则:从最稳定到最不稳定。基础镜像、系统依赖、语言依赖、配置文件、业务代码,越不容易变的越靠前。反例则是把 COPY . . 放在 pip install 之前——每次改一行代码都要重装全部依赖,构建从 20 秒变 8 分钟。

⚠️ 还有个新手黑洞:.dockerignore 不写好,COPY . . 会把本地 git 历史、node_modules、日志统统打进镜像,既拖慢构建又污染镜像。构建上下文里放一个 .dockerignore

.git node_modules *.log .env

用一次实验把缓存钉死在脑子里

# 第一次:全部新建 docker build -t cache-demo . # 第二次:什么都不改,全部 CACHED docker build -t cache-demo . # 第三步:touch 一个无关紧要的文件再构建 echo "# note" >> README.md && docker build -t cache-demo .

第三次构建里,COPY . . 之前的层命中,从 COPY . . 起重建。对照输出里的 CACHED 标记与 2.1 节讲的"指令产层",你会看到缓存其实就是"层能不能复用"这个问题的答案。

本节要点回顾

  • 指令产层、缓存逐层匹配:第一条变化的指令之后全部重建
  • 依赖文件与业务代码分开 COPY:让重依赖层躲开代码变更
  • RUN 合并、装完即清:避免层间垃圾与索引错位
  • ADD 一律换 COPY,生产入口一律 exec 数组形式,否则 SIGTERM 收不到
  • 从稳定到不稳定排序指令,配好 .dockerignore

层造出来了,接下来解决它怎么高效分发、怎么减肥。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U