Dockerfile构建容器镜像实践
一、意义
容器镜像作为我们实际运行服务的载体,它的大小有极大意义,主要体现在以下方面:
- 更小的镜像,极大节省我们的存储开销。
- 缩短部署时的镜像传输时间,编译后的上传和下载都能更快些。
- 提升安全性,减少可供攻击的目标。
- 减少故障恢复时间。
二、Dockerfile 详解
2.1、构建规则
- 选择基础镜像应尽可能小
- 非必要目录应忽略(git目录、node_modules 目录等)
- 减少 RUN/ADD/COPY指令
2.2、联合文件系统
分层的实现原理,核心依赖两大技术:联合文件系统(UnionFS) 和 写时复制(Copy-on-Write)。
实现 “多层合并为统一视图”,联合文件系统是一种特殊的文件系统,它能将多个独立的目录(称为 “分支”)挂载到同一个目录下,形成一个虚拟的统一文件系统。用户访问该目录时,看到的是所有分支文件的 “合并结果”,而非单个分支。
Docker 中常用的联合文件系统变体包括:overlay2(目前最主流)、devicemapper、btrfs 等,其中 overlay2 因性能优势被广泛采用。
UnionFS 的核心特性:层的叠加与覆盖
- 多层中若存在同名文件,上层文件会覆盖下层文件(用户只能看到上层版本);
- 若文件仅存在于下层,上层未修改,则用户看到的是下层文件;
- 合并过程是 “虚拟” 的,不实际复制文件,仅通过元数据记录层间关系,因此高效且节省空间。
示例:
- 层 1(底层)包含
a.txt(内容:hello)、b.txt; - 层 2(上层)包含
a.txt(内容:world); - 联合挂载后,用户看到的文件系统中:
a.txt是层 2 的world,b.txt是层 1 的内容。
2.3、写时复制
实现 “只读层保护与容器修改隔离”,写时复制是 Docker 保证镜像只读性和容器修改隔离的关键机制。其核心逻辑是:当容器需要修改只读层中的文件时,先将文件从只读层复制到可写层,再修改可写层的副本,原只读层的文件保持不变。
详细流程(以修改文件为例):
- 容器尝试修改文件
a.txt,该文件原本存在于镜像的只读层 1 中; - Docker 检查到
a.txt来自只读层,立即将其从只读层 1 复制到容器的可写层; - 容器对可写层中的
a.txt副本进行修改(原只读层的a.txt未被触碰); - 后续访问
a.txt时,联合文件系统会优先展示可写层的修改后版本。
优势:
- 保护镜像只读层不被修改,确保镜像的一致性和可复用性;
- 多个容器共享同一镜像时,仅在各自的可写层维护修改,避免重复占用空间(如 10 个容器基于同一镜像启动,仅存储 1 份镜像层 + 10 份各自的可写层修改)。
2.4、镜像分层 - 只读层
Dockerfile 中只会形成只读层,属于 Docker 镜像,是镜像组成部分(镜像由多个只读层堆叠而成)。
读写权限: 完全只读,任何操作都无法修改其内容(保证镜像一致性)。 生命周期: 与镜像绑定,镜像存在则层存在。 来源: 由Dockerfile指令(RUN/ADD/COPY)生成,记录文件系统的增量变更(如安装的依赖、复制的代码) 共享性: 可被多个镜像或容器共享(如多个镜像基于同一基础层,基础层仅存层一次)
Dockerfile 中的 RUN、ADD、COPY 指令会创建新的镜像层,这些层是镜像的组成部分,属于只读层。
- 镜像的本质是只读的: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