跳到主要内容

Dockerfile构建容器镜像实践

阅读需 7 分钟

Dockerfile构建容器镜像实践

一、意义

容器镜像作为我们实际运行服务的载体,它的大小有极大意义,主要体现在以下方面:

  • 更小的镜像,极大节省我们的存储开销。
  • 缩短部署时的镜像传输时间,编译后的上传和下载都能更快些。
  • 提升安全性,减少可供攻击的目标。
  • 减少故障恢复时间。

二、Dockerfile 详解

2.1、构建规则

  • 选择基础镜像应尽可能小
  • 非必要目录应忽略(git目录、node_modules 目录等)
  • 减少 RUN/ADD/COPY指令

2.2、联合文件系统

分层的实现原理,核心依赖两大技术:联合文件系统(UnionFS) 和 写时复制(Copy-on-Write)

实现 “多层合并为统一视图”,联合文件系统是一种特殊的文件系统,它能将多个独立的目录(称为 “分支”)挂载到同一个目录下,形成一个虚拟的统一文件系统。用户访问该目录时,看到的是所有分支文件的 “合并结果”,而非单个分支。

Docker 中常用的联合文件系统变体包括:overlay2(目前最主流)、devicemapperbtrfs 等,其中 overlay2 因性能优势被广泛采用。

UnionFS 的核心特性:层的叠加与覆盖

  • 多层中若存在同名文件,上层文件会覆盖下层文件(用户只能看到上层版本);
  • 若文件仅存在于下层,上层未修改,则用户看到的是下层文件;
  • 合并过程是 “虚拟” 的,不实际复制文件,仅通过元数据记录层间关系,因此高效且节省空间。

示例

  • 层 1(底层)包含 a.txt(内容:hello)、b.txt
  • 层 2(上层)包含 a.txt(内容:world);
  • 联合挂载后,用户看到的文件系统中:a.txt 是层 2 的 worldb.txt 是层 1 的内容。

2.3、写时复制

实现 “只读层保护与容器修改隔离”,写时复制是 Docker 保证镜像只读性和容器修改隔离的关键机制。其核心逻辑是:当容器需要修改只读层中的文件时,先将文件从只读层复制到可写层,再修改可写层的副本,原只读层的文件保持不变

详细流程(以修改文件为例):

  1. 容器尝试修改文件 a.txt,该文件原本存在于镜像的只读层 1 中;
  2. Docker 检查到 a.txt 来自只读层,立即将其从只读层 1 复制到容器的可写层;
  3. 容器对可写层中的 a.txt 副本进行修改(原只读层的 a.txt 未被触碰);
  4. 后续访问 a.txt 时,联合文件系统会优先展示可写层的修改后版本。

优势

  • 保护镜像只读层不被修改,确保镜像的一致性和可复用性;
  • 多个容器共享同一镜像时,仅在各自的可写层维护修改,避免重复占用空间(如 10 个容器基于同一镜像启动,仅存储 1 份镜像层 + 10 份各自的可写层修改)。

2.4、镜像分层 - 只读层

Dockerfile 中只会形成只读层,属于 Docker 镜像,是镜像组成部分(镜像由多个只读层堆叠而成)。

读写权限: 完全只读,任何操作都无法修改其内容(保证镜像一致性)。 生命周期: 与镜像绑定,镜像存在则层存在。 来源: 由Dockerfile指令(RUN/ADD/COPY)生成,记录文件系统的增量变更(如安装的依赖、复制的代码) 共享性: 可被多个镜像或容器共享(如多个镜像基于同一基础层,基础层仅存层一次)

Dockerfile 中的 RUNADDCOPY 指令会创建新的镜像层,这些层是镜像的组成部分,属于只读层

  • 镜像的本质是只读的:Docker 镜像设计为 “不可变” 的模板,用于创建容器。RUN(执行命令安装依赖)、ADD/COPY(复制文件)等指令的作用是 “构建镜像的内容”,这些内容一旦固化为层,就不允许修改(否则会破坏镜像的一致性和可复用性)。
  • 分层构建的逻辑: 每个 RUN/ADD/COPY 指令的执行结果会被打包为一个独立的只读层,叠加在之前的层之上。
  • 与容器可写层的交互: 当容器从镜像启动时,这些只读层被联合文件系统挂载,容器对文件的修改会通过 “写时复制” 机制转移到可写层,而原始的只读层(包括 RUN/ADD 生成的层)始终保持不变。

2.5、镜像分层 - 可写层

属于容器实例,每个容器独立拥有一个可写层,可写层是容器启动时新增的临时层,仅属于当前容器,可读可写,用于隔离容器的所有修改,生命周期与容器绑定。

生命周期: 与容器绑定,容器创建时生成,容器删除时可写层也被删除。 读写权限: 可读写(Read-Write),容器内所有文件修改(新增 / 修改 / 删除)都发生在此层。 来源: 初始为空,内容来自:1. 从只读层复制的待修改文件;2. 容器内新增的文件;3. 被删除文件的 “删除标记”。 共享性: 不可共享,每个容器的可写层是独立的(容器隔离的核心)。 对镜像的影响: 可写层的修改不会影响原始镜像,仅存在于容器生命周期内。

关键细节:可写层如何处理 “删除文件”

当容器删除一个来自只读层的文件时,可写层不会真正删除底层文件(因为只读层不可修改),而是通过创建 “删除标记(whiteout)” 实现:

  • 可写层中生成一个特殊的标记文件(如 .wh.a.txt),告诉联合文件系统 “隐藏下层的 a.txt”;
  • 用户在容器内看不到 a.txt,但底层只读层的 a.txt 依然存在(其他容器仍可访问)。

Docker 分层机制的本质是:通过联合文件系统将多个只读镜像层合并为统一视图,通过写时复制机制在容器启动时添加可写层,实现镜像的只读保护、容器修改隔离和资源高效利用

2.6、忽略文件 .dockerignore

使用.dockerignore 文件在构建时就可以避免将本地模块以及调试日志被拷贝进入到Docker镜像中。

三、多阶段构建

多阶段构建非常适用于编译性语言,允许一个Dockerfile中出现多条FROM指令,只有最后一条FROM指令中指定的基础镜像作为本次构建镜像的基础镜像,其它的阶段都可以认为是只为中间步骤。

ROM … AS …COPY --from组合使用

选用更小的基础镜像

  • scratch:空镜像,又叫镜像之父。
  • busybox:对比scratch,多了常用的linux工具等
  • alpine:多了包管理工具apk

对于很多编程语言,可能会依赖底层库 glibc ,这时 alpine 就需要进行改造,安装 glibc 才行。 可以参考: https://github.com/Docker-Hub-frolvlad/docker-alpine-glibc

四、镜像分析

docker history image_xxxx

五、常用 docker 命令

docker build

可写层生成新镜像:docker commit