【AI 总结】Docker Build Practice
Docker Build Practice | AI 总结
一、镜像构建最佳实践
1. 使用多阶段构建
- 核心优势:分离构建过程与最终输出,减少最终镜像体积,仅保留应用运行必需文件;支持并行执行构建步骤,提升构建效率。
- 进阶技巧:针对多个存在大量共性的镜像,创建包含共享组件的可复用阶段,Docker 仅需构建一次公共阶段,能提升衍生镜像在 Docker 主机上的内存使用效率与加载速度,同时降低维护成本(遵循 “不要重复自己” 原则)。
1 | |
在 Docker 构建中,多阶段构建(Multi-stage builds) 能够减少体积和提升效率,主要源于其 “按需取舍” 和 “并行处理” 的机制。
以下是详细解释:
1. 为什么能减少镜像体积?
在传统的单阶段构建中,所有为了构建应用而引入的工具都会保留在最终镜像中。而多阶段构建通过 “只拷贝产物” 解决了这个问题:
- 分离构建环境与运行环境:
- 第一阶段(构建阶段):你可以使用一个包含完整编译器、SDK、源代码和各种构建工具(如
gcc、golang、npm、maven)的庞大基础镜像。在这个阶段,你生成了可执行文件或静态资源。 - 第二阶段(最终阶段):你切换到一个极其精简的基础镜像(如
alpine或scratch),它们不含任何开发工具。
- 第一阶段(构建阶段):你可以使用一个包含完整编译器、SDK、源代码和各种构建工具(如
- 只保留必要文件:通过
COPY --from=build指令,你只从第一阶段把编译好的 “二进制文件” 或 “生产环境包” 拷贝过来。 - 丢弃冗余层:第一阶段产生的所有中间层(包括数 GB 的依赖包、临时构建缓存、源代码)都会被 Docker 丢弃,不会占用最终镜像的任何空间。
类比:就像在厨房做饭(构建阶段),你需要各种锅碗瓢盆和调料,但最后端到客人桌上的(最终镜像)只有那盘菜,那些沉重的厨具并不随菜一起端上去。
2. 为什么能提升构建效率?
多阶段构建配合现代的构建引擎(如 BuildKit),在效率上有显著提升:
- 支持并行执行 (Parallelism)
- 如果你的
Dockerfile中有多个互不依赖的阶段(例如:一个阶段编译前端静态资源,另一个阶段编译后端 Go 代码),Docker 可以同时启动这些阶段进行构建,而不是一行行按顺序执行。
- 如果你的
- 更精细的缓存控制:
- Docker 是按层缓存的。在多阶段构建中,如果某个阶段的代码没有变动,Docker 可以直接跳过整个阶段,使用缓存。
- 例如,如果你只改动了前端代码,那么后端编译阶段的缓存依然有效,大大缩短了重新构建的时间。
- 减少数据传输:
- 因为最终生成的镜像极小,在 CI/CD 流水线中推送(Push)到镜像仓库,或者在服务器上拉取(Pull)镜像的速度都会快得多。
总结
- 减重:通过
FROM指令切换镜像,实现 “卸磨杀驴”,只留结果,不留工具。 - 加速:通过 “多线作战” 和 “精准缓存”,减少重复劳动。
“不要重复自己”(Don’t Repeat Yourself,简称 DRY)是软件工程中的一个核心原则。它的核心思想是:在一个系统中,每一处知识(或逻辑)都必须有单一、明确、权威的表示。
在您提到的 Docker 构建场景中,这个原则的具体含义和好处可以从以下几个方面来理解:
1. 减少冗余 (Reduce Redundancy)
如果在多个 Dockerfile 中都写了相同的安装脚本(比如安装编译工具、配置环境变量),那么你就 “重复了自己”。
- 做法:将这些公共部分提取出来,创建一个基础镜像(Base Image)或者在多阶段构建中定义一个公共阶段(Common Stage)。
- DRY 表现:公共逻辑只写一次。
2. 降低维护成本 (Lower Maintenance Cost)
假设你需要更新一个安全补丁或更改一个依赖版本:
- 如果没有遵循 DRY:你需要手动修改每一个 Dockerfile。漏掉一个就会导致环境不一致甚至安全隐患。
- 如果遵循了 DRY:你只需要修改那个 “公共阶段” 或 “基础镜像”,所有依赖它的镜像在重新构建时都会自动获得更新。
3. 提升构建效率与性能
Docker 的镜像分层存储机制非常契合 DRY 原则:
- 共享层:当你针对多个镜像提取出公共阶段后,Docker 只需要构建并存储这个阶段一次。
- 磁盘与内存效率:在同一台宿主机上,多个运行中的容器如果基于相同的公共层,它们可以共享这部分内存和磁盘空间。
- 加快分发:当你推送(Push)或拉取(Pull)镜像时,已经存在的公共层会被跳过,大大节省带宽和时间。
举个例子
重复自己 (Bad):
- 镜像 A 的 Dockerfile:
RUN apt-get update && apt-get install -y python3… - 镜像 B 的 Dockerfile:
RUN apt-get update && apt-get install -y python3…
不重复自己 (Good - DRY):
1 | |
总结: 在文档的语境下,“不要重复自己” 就是让你把重复的操作提取出来,变 “多次编写、多次构建” 为 “一次编写、一次构建、多次复用”。
2. 选择合适的基础镜像
- 优先选择来源:
- Docker 官方镜像(Docker Official Images):经过精心筛选,文档清晰,遵循最佳实践且更新频繁,是可靠的起始选择。
- 已验证发布者镜像(Verified Publisher):由与 Docker 合作的组织发布维护,Docker 验证其仓库内容真实性,质量有保障。
- Docker 赞助的开源镜像(Docker-Sponsored Open Source):由 Docker 赞助的开源项目发布维护。
- 关键特性要求:选择最小化基础镜像,以提升可移植性、加快下载速度,同时减少镜像体积与依赖引入的漏洞;开发与生产环境可使用不同基础镜像,生产环境优先选择更精简的镜像,移除编译器、调试工具等非必需组件,降低攻击面。
选择最小化基础镜像的核心逻辑是在 “运行环境需求” 与 “镜像体积 / 安全性” 之间寻找平衡点。以下是判断和选择最小基础镜像的几个步骤和标准:
1. 明确应用运行的底层依赖
首先要分析你的应用(如 Go, Java, Python, Node.js)究竟需要什么:
- 编译后是否为静态链接:例如 Go 编写的应用,如果使用了静态编译,它几乎不需要任何底层库。
- 运行时依赖:是否需要特定的动态库(如
glibc或musl)、时区文件(tzdata)或根证书(ca-certificates)。 - 运维需求:是否需要在容器内执行
ls、sh等命令进行调试。
2. 常见基础镜像的 “分级” 选择
根据 “最小化” 程度,通常有以下几种选择路径:
A. scratch(绝对最小)
- 特点:空镜像,体积为 0。
- 适用场景:静态编译的可执行文件(如 Go, Rust)。
- 判断标准:如果你的程序不需要 Shell,也不需要任何外部库,直接用
FROM scratch。
B. alpine(通用最小)
- 特点:基于 musl libc 和 busybox,体积约 5MB。
- 适用场景:大多数 Web 应用、脚本语言环境。
- 注意点:它使用
musl而不是常用的glibc。如果你的程序依赖glibc(如某些 Python 科学计算库或 Oracle 驱动),可能会运行失败。
C. distroless(Google 推出的无发行版镜像)
- 特点:只包含应用程序及其运行时依赖,不包含包管理器、Shell 或其他程序。
- 适用场景:追求极致安全且不需要在容器内调试的生产环境。
- 判断标准:当你只需要运行 Java、Node 或 Python 且不想引入 Shell 带来的安全漏洞时。
D. 官方镜像的 -slim 版本
- 特点:基于 Debian/Ubuntu,删除了开发工具和说明文档,体积通常在 30MB-100MB 之间。
- 适用场景:应用强依赖
glibc,或在 Alpine 下存在兼容性问题。
3. 如何判断 “能否使用”?(验证步骤)
要确定一个镜像是否是 “所需且能用” 的最小值,可以按以下流程操作:
从 Alpine 开始尝试
- 如果应用能正常启动并执行核心功能,那么 Alpine 就是首选。
- 报错
File not found? 这通常是glibc缺失的表现(因为 musl 找不到对应路径)。此时可考虑换成debian:slim。
检查库依赖 (
ldd)- 在开发环境下,对你的二进制文件执行
ldd <binary>。 - 观察它引用的
.so文件。如果列表很短且你能手动拷贝这些库到scratch或alpine中,那就可以进一步精简。
- 在开发环境下,对你的二进制文件执行
使用工具分析镜像层级
- 使用
docker history <image>查看镜像的每一层大小。 - 推荐工具:dive 。它可以让你看到镜像内部的文件结构,帮助你发现哪些文件是冗余的。
- 使用
4. 总结判断准则
你可以参考这个决策链:
- 它是 Go/Rust 静态编译吗? → 是 →
scratch - 它对 glibc 有强依赖吗? → 否 →
alpine - 它是 Java/Node/Python 生产环境吗? → 是 →
distroless - 以上都不行? → 选择官方镜像的
-slim标签。
提示:“开发与生产环境使用不同基础镜像”,就是典型的多阶段构建(Multi-stage build)策略。你在第一阶段用体积大的镜像(含编译器)构建,第二阶段将产物拷贝到上述的 “最小镜像” 中即可。
3. 定期重建镜像
镜像不可变,构建时会快照当前所有依赖(包括基础镜像、库等),定期重建可确保镜像依赖更新,保障安全性与时效性。
使用 --pull 选项
构建时强制 Docker 检查并下载基础镜像的最新版本,即使本地有缓存,例如
1 | |
使用 --no-cache 选项
禁用构建缓存,强制从 scratch 重建所有层,获取包管理器(如 apt-get、npm)的最新依赖,但不影响基础镜像,命令为
1 | |
组合使用:docker build --pull --no-cache -t my-image:my-tag .,实现基于最新基础镜像的完全干净构建。
这两者虽然都涉及到 “不再使用旧的东西”,但它们作用的对象和阶段完全不同。简单来说:
--pull:针对的是 “地基”(基础镜像)--no-cache:针对的是 “施工过程”(Dockerfile 中的每一行指令)
以下是具体的区别对比:
1. --pull (拉取最新基础镜像)
- 作用对象:
FROM指令中指定的镜像。 - 行为: 在开始构建之前,Docker 会去远程仓库(如 Docker Hub)检查你的基础镜像(例如
FROM python:3.9)是否有更新。 - 场景: 即使本地已经有了
python:3.9镜像,如果远程仓库的这个标签指向了新的补丁版本(比如修复了安全漏洞),--pull会强制下载最新的镜像。 - 如果不加: 如果本地已有该镜像,Docker 就会直接用本地的,而不管云端是否有更新。
2. --no-cache (禁用指令缓存)
- 作用对象: Dockerfile 中
FROM之后的每一行指令(RUN,COPY,ADD等生成的层)。 - 行为: 正常构建时,如果某一行指令没变且文件没变,Docker 会直接复用上次构建的结果(显示
CACHED)。--no-cache会强制 Docker 重新执行所有指令。 - 场景: 最典型的例子是
RUN apt-get update && apt-get install -y some-package。如果不用--no-cache,Docker 可能会复用一周前的缓存结果,导致你装不到最新的安全补丁。 - 如果不加: Docker 会尽量复用缓存以提高构建速度。
形象的比喻
假设你要按照 “菜谱” 做一盘 “宫保鸡丁”:
--pull:相当于强制去超市买最新鲜的食材(基础镜像),而不是用冰箱里剩的旧食材。--no-cache:相当于要求你严格按照步骤重做一遍,不准直接拿昨天做好的半成品(缓存层)来加热。
什么时候两个一起用?
如果你希望完全从零开始、确保所有内容都是最新的,通常会组合使用:
1 | |
这样既保证了 “地基”(基础镜像)是最新的,也保证了 “装修过程”(每一行命令)都是重新执行且获取了最新的依赖包。
4. 利用 .dockerignore 排除文件
- 功能作用:无需重构源码仓库,通过类似
.gitignore的排除规则,过滤构建无关文件,减少构建上下文大小,提升构建效率。 - 示例:若要排除所有
.md格式文件,在.dockerignore中添加*.md即可。
5. 创建临时容器
- 核心目标:使 Dockerfile 定义的镜像生成的容器具备 “临时” 特性,即容器可随时停止、销毁,重建与替换时仅需极少的配置与设置。
- 参考标准:遵循《十二因素应用》(The Twelve-factor App)中 “进程”(Processes)相关方法论,实现容器无状态运行。
1. 什么是 “临时容器” (Ephemeral Containers)?
这里的 “临时” 不是指容器运行时间短,而是指容器的生命周期是可替换的。
- 不可变性:你不应该在运行中的容器内手动修改配置或安装软件。
- 可丢弃性:如果容器出故障了,或者需要更新版本,你应该能够直接
docker rm -f删掉它,然后用同样的镜像立刻起一个新的,且新容器能正常工作。
2. 核心目标的具体含义
“容器可随时停止、销毁,重建与替换时仅需极少的配置与设置”
这意味着:
- 配置分离:所有的配置(如数据库地址、API 密钥)应该通过环境变量(Environment Variables)或挂载文件(Volumes)传入,而不是硬编码在镜像里。
- 数据持久化:容器本身不存储持久化数据(如数据库文件、用户上传的照片)。重要数据必须存储在容器外的持久化卷(Volumes)或外部服务中。这样销毁容器时,数据不会丢失。
3. 参考《十二因素应用》(The Twelve-Factor App)
Twelve-Factor App 是一套构建 SaaS 应用的原则。其中 “进程 (Processes)” 这一条指出:
“运行环境应该是无状态的,且不共享任何东西。”
- 无状态 (Stateless) :容器在处理请求时不应该依赖于内存中或本地磁盘上的残留数据。
- 后果:如果你的应用是无状态的,你就可以轻松地水平扩展(同时运行 10 个一样的容器),因为任何一个请求发给任何一个容器都能得到正确处理。
举个例子
- 反面教材(有状态):你把用户上传的头像保存在容器内的
/app/uploads文件夹里。当你更新镜像重启容器时,之前的头像全丢了。 - 正面教材(无状态 / 临时):你把头像上传到专门的对象存储(如 AWS S3),或者挂载一个外部卷。容器被销毁重建后,依然能从相同的地方读取到数据。
总结:这一段的意思是,我们要像对待 “牲畜”(随时替换)一样对待容器,而不是像对待 “宠物”(细心呵护、不能弄丢)一样。这是云原生应用和微服务架构的基础。
6. 不安装不必要的包
- 原则要求:仅安装应用运行必需的包,例如数据库镜像无需包含文本编辑器。
- 带来益处:降低镜像复杂度与依赖数量,减小镜像体积,缩短构建时间。
7. 解耦应用
- 核心原则:每个容器仅负责一个关注点,将应用拆分为多个容器,便于水平扩展与容器复用。
- 实例场景:Web 应用栈可拆分为 Web 应用、数据库、内存缓存三个独立容器,各容器使用专属镜像。
- 灵活处理:虽建议单容器单进程,但非强制规则,部分程序(如 Celery、Apache)会自动生成多进程;容器间若有依赖,可通过 Docker 容器网络实现通信。
这是一个非常深刻且实际的问题。在 Docker 的实践中,“解耦(Decoupling)” 确实是一把双刃剑。
我们可以从以下几个维度来辩证地看待这个问题:
1. 为什么要解耦?(解耦的红利)
解耦的核心动力在于:
- 独立扩展(Horizontal Scaling):如果你的 Web 应用压力大,你可以只增加 Web 容器的数量,而不需要多开一个数据库容器。如果所有东西都在一个容器里,你只能整体扩容,这非常浪费资源。
- 版本控制与重用:比如你的数据库(Redis/MySQL)可以保持不变,只更新业务代码的镜像。而且你可以很方便地在另一个项目里复用这个 Redis 镜像。
- 故障隔离:Web 应用崩溃了,不会导致数据库也跟着重启。日志也更容易区分,你可以清晰地看到是哪个组件出了问题。
2. 管理确实变难了吗?(复杂度的转移)
你担心的 “管理麻烦” 是完全正确的。复杂度并没有消失,只是转移了。
- 单容器模式:复杂度在镜像内部。你需要写复杂的
entrypoint.sh脚本来管理多个进程,处理它们的启动顺序、僵尸进程、日志混合等问题。 - 多容器模式:复杂度在容器编排(Orchestration)。你需要管理网络连接、数据卷共享、启动依赖(比如数据库必须在应用之前启动)。
3. 如何解决 “管理麻烦”?
如果你还在手动用 docker run 一个个启动容器,那确实是 “运维地狱”。现代容器技术有一套成熟的工具链来解决这个麻烦:
- 开发环境:Docker Compose
通过一个docker-compose.yml文件,你可以一行命令docker-compose up同时启动 Web、DB、Redis。它自动处理了网络连接(Container Networking),你只需要在代码里用服务名(如db:3306)就能访问,不需要管 IP。 - 生产环境:Kubernetes (K8s)
虽然 K8s 学习曲线陡峭,但它专门解决 “大量容器管理” 的问题,包括自动扩缩容、自愈(容器挂了自动重启)、服务发现等。
4. 灵活处理:不要过度设计
虽然建议单容器单进程,但非强制规则。
建议的衡量标准是:
- 生命周期是否一致? 如果两个进程必须同时生死(比如主程序和它的本地监控插件),可以考虑放在一起。
- 是否需要独立扩容? 如果不需要,且耦合极深,放在一个容器里也未尝不可。
总结建议
看待解耦应用,不应该把它看作 “为了拆分而拆分” 的教条,而应该看作是为了获得 “可伸缩性” 和 “灵活性” 而付出的代价。
- 对于小型项目:用 Docker Compose 管理 3-5 个容器,复杂度是完全可以接受的,带来的开发环境一致性收益远大于管理成本。
- 对于巨型项目:不拆分意味着无法维护。
- 对于极简应用:如果就是一个简单的脚本,确实没必要强行拆分。
你现在在阅读的这份《Dockerfile 最佳实践》更多是站在工程化和专业运维的角度给出的 “上限” 建议,实际操作中可以根据项目规模和团队的运维能力进行折中。
8. 对多行参数排序
- 操作要求:多行参数尽量按字母数字顺序排列,且在反斜杠(
\)前加空格。 - 优势体现:避免包重复,便于更新参数列表,同时让代码审查(PR)更易读、更高效。
1 | |
9. 利用构建缓存
- 工作机制:Docker 按 Dockerfile 指令顺序执行,每步会检查是否可复用缓存。
- 重要性:理解缓存工作原理与失效机制,是实现快速构建的关键。
在 Docker 构建过程中,缓存机制是大幅提升构建速度的核心。理解其工作原理和失效规则,可以帮助我们编写出更高效的 Dockerfile。
1. 缓存工作原理:基于指令的层级匹配
Docker 构建镜像时,会逐行读取 Dockerfile 中的指令。对于每一条指令,Docker 都会在本地缓存中寻找是否已经存在一个基于相同 “父层” 且执行了相同指令的镜像层(Layer)。
- 匹配规则:Docker 会检查当前指令的内容以及它所依赖的父镜像。如果指令字符串未发生变化,且父镜像层完全一致,Docker 就会直接复用缓存。
- 链式反应:缓存是逐层叠加的。如果某一层缓存未命中(Cache Miss),那么该指令之后的所有后续指令都无法再使用缓存,必须重新执行。
2. 缓存失效机制:什么会导致缓存失效?
理解失效机制是优化构建的关键。以下几种情况会触发缓存失效:
A. 指令内容发生变化
这是最直接的情况。如果你修改了 Dockerfile 中的某一行代码(哪怕只是加了一个空格或注释),该行及其后的所有指令缓存都会失效。
- 示例:将
RUN apt-get update修改为RUN apt-get update && apt-get upgrade。
B. ADD 或 COPY 引入的文件发生变化
对于 ADD 和 COPY 指令,Docker 不仅检查指令字符串,还会计算指令中涉及的文件内容(Checksum / 散列值)。
- 检测逻辑:Docker 会计算构建上下文中每个文件的校验和。如果任何一个文件的内容发生了变化,对应的
ADD/COPY指令缓存就会失效。 - 注意:文件的 “最后修改时间” 或 “最后访问时间” 不会被计入校验和,只有内容本身决定失效。
C. 构建参数(Build Args)的变化
如果在指令中使用了 ARG 定义的变量,且在构建时通过 --build-arg 传入了不同的值,那么引用了该变量的指令缓存会失效。
D. 外部资源的变动(无法自动感知)
需要注意的是,对于 RUN apt-get update 这种从网络下载资源的指令,Docker 无法感知远程服务器上的数据是否变化。
- 风险:如果 Docker 指令没变,它会一直复用之前的缓存,导致你安装的软件版本可能非常陈旧。
- 解决建议:通过合并指令(如
RUN apt-get update && apt-get install -y ...)来确保一旦依赖列表变化就触发更新。
3. 优化建议:如何高效利用缓存?
为了最大化缓存命中率,建议遵循以下原则:
- 将 “不变” 的层放在前面,“变动” 的层放在后面:
- 先安装基础系统依赖、工具类软件。
- 中间层安装项目依赖(如
package.json,requirements.txt)。 - 最后才拷贝源代码(
COPY . .),因为源代码通常变动最频繁。
- 合并相关的指令:
- 将
apt-get update和apt-get install放在同一个RUN指令中,防止因为缓存导致安装了过期的软件索引。
- 将
- 精确拷贝文件:
- 不要直接
COPY . .。如果你只需要安装依赖,先COPY package.json .,安装完依赖后再拷贝其余代码。这样代码改动时,依赖层依然可以命中缓存。
- 不要直接
- 使用
.dockerignore- 通过
.dockerignore排除掉.git、日志、临时文件等,防止这些无关文件的变动意外导致COPY指令缓存失效。
- 通过
你可以考虑将这些详细内容补充到你的 Markdown 文档中,或者作为该章节的展开说明。
10. 固定基础镜像版本
- 标签特性:镜像标签可变,发布者可能更新标签指向新镜像,虽能自动获取新版本,但无法保证构建一致性,也缺乏版本审计跟踪。
- 固定方法:通过镜像摘要(digest)固定版本,确保即使标签更新,构建仍使用指定版本,例如
FROM alpine:3.21@sha256:a8560b36e8b8210634f77d9f7f9efd7ffa463e380b75e2e74aff4511df3ef88c。 - 自动化工具:Docker Scout 的 “最新基础镜像”(Up-to-Date Base Images)策略可检查基础镜像版本是否最新及摘要是否正确;若基础镜像更新,还能自动创建 PR 更新 Dockerfile 中的镜像版本,兼顾版本控制与审计跟踪。
11. 在 CI 中构建与测试镜像
- 自动化要求:代码提交至版本控制或创建 PR 时,借助 GitHub Actions 或其他 CI/CD 流水线,自动构建、标记 Docker 镜像并执行测试。
二、Dockerfile 指令规范
1. FROM
- 使用建议:优先以当前官方镜像为基础,推荐 Alpine 镜像(体积小于 6MB,是完整 Linux 发行版,且管控严格)。
2. LABEL
- 功能用途:为镜像添加标签,用于按项目分类、记录许可信息、辅助自动化等。
- 语法格式:支持单行单标签、单行多标签、多行多标签(使用续行符),含空格的字符串需加引号或转义,内部引号也需转义。
- 历史变化:Docker 1.10 前建议将所有标签合并为一个 LABEL 指令以避免多余层,现虽无强制要求,但合并写法仍受支持。
3. RUN
- 格式优化:长或复杂的 RUN 语句需用反斜杠拆分为多行,提升可读性与可维护性。
- 命令执行:可通过
&&链式执行命令,也可使用 here documents 执行多命令,无需管道符。 - apt-get 专项建议:
- 需将
apt-get update与apt-get install合并在同一 RUN 指令中,避免缓存问题导致依赖版本过时。 - 可通过指定包版本(版本固定)实现缓存突破,减少依赖变更引发的构建失败。
- 安装后建议删除
/var/lib/apt/lists/*清理 apt 缓存,减小镜像体积(Debian、Ubuntu 官方镜像会自动执行apt-get clean,无需手动操作)。
- 需将
- 管道命令处理:默认
/bin/sh -c仅判断管道最后一步命令的退出码,若需管道中任一命令失败则整体失败,可在命令前加set -o pipefail &&;若 shell 不支持该选项,可使用 exec 形式指定支持的 shell(如/bin/bash)。
4. CMD
- 核心用途:运行镜像包含的软件,通常使用
CMD ["executable", "param1", "param2"]格式。 - 场景适配:服务类镜像(如 Apache、Rails)可指定服务启动命令(如
CMD ["apache2","-DFOREGROUND"]);交互类场景(如 bash、Python)可指定交互 shell(如CMD ["python"])。 - 使用限制:除非熟悉 ENTRYPOINT 工作机制,否则避免与 ENTRYPOINT 配合使用
CMD ["param", "param"]格式。
5. EXPOSE
- 功能定义:声明容器监听连接的端口,需使用应用的常规标准端口(如 Apache 用 80,MongoDB 用 27017)。
- 端口映射:外部访问时,用户可通过
docker run选项将声明端口映射到自定义端口;容器链接时,Docker 会提供环境变量(如MYSQL_PORT_3306_TCP)标识源容器端口路径。
6. ENV
- 主要作用:更新环境变量(如 PATH,确保
CMD ["nginx"]等命令可直接运行)、设置服务必需环境变量(如 Postgres 的 PGDATA)、定义常用版本号(便于版本更新维护)。 - 层特性:每个 ENV 指令会创建新中间层,即使后续层 unset 变量,该变量仍会保留在原层;若需彻底清除变量,需在单个 RUN 指令中完成变量的设置、使用与 unset。
7. ADD 与 COPY
- 功能差异:
- COPY:支持从构建上下文或多阶段构建的某阶段向容器复制文件。
- ADD:除 COPY 功能外,还支持从远程 HTTPS、Git URL 下载文件,且能自动解压构建上下文中的 tar 文件。
- 使用场景:
- 多阶段构建中跨阶段复制文件优先用 COPY;临时添加文件供 RUN 指令使用,可替换为绑定挂载(更高效,且文件不保留在最终镜像);需在构建中下载远程资源时用 ADD,其支持校验和验证与 Git 资源解析,缓存精度更高。
8. ENTRYPOINT
- 最佳用法:设置镜像的主命令,使镜像可像该命令一样运行,配合 CMD 指定默认参数。
- 实例演示:s3cmd 工具镜像可设
ENTRYPOINT ["s3cmd"]、CMD ["--help"],运行docker run s3cmd可显示帮助,运行docker run s3cmd ls s3://mybucket可执行具体命令。 - 脚本配合:可与辅助脚本结合,实现复杂启动逻辑(如 Postgres 官方镜像的入口脚本),脚本中建议使用
exec命令使应用成为容器 PID 1,确保能接收 Unix 信号。
9. VOLUME
- 应用场景:用于暴露容器的数据库存储区、配置存储区、生成的文件目录等,尤其推荐用于镜像中可变或用户可维护的部分。
10. USER
- 安全建议:若服务无需特权,可切换为非 root 用户,需先通过
RUN groupadd -r 组名 && useradd --no-log-init -r -g 组名 用户名创建用户组与用户。 - 注意事项:建议指定明确 UID/GID(避免重建镜像时 UID/GID 变化);避免使用 sudo(TTY 和信号转发行为不可预测),若需类似 sudo 功能(如 root 初始化 daemon 后切换非 root 运行),可使用 gosu;减少 USER 指令切换频率,降低镜像复杂度与层数。
11. WORKDIR
- 路径要求:必须使用绝对路径,且优先用 WORKDIR 替代
RUN cd … && do-something这类难维护、难排查的指令。
12. ONBUILD
- 执行时机:当前 Dockerfile 构建完成后,在基于该镜像构建子镜像时,会先执行 ONBUILD 指令(在子镜像 Dockerfile 命令前执行)。
- 适用场景:适用于构建语言栈镜像(如 Ruby 的 ONBUILD 变体),便于子镜像构建对应语言的用户软件。
- 使用规范:含 ONBUILD 的镜像需单独打标签(如
ruby:1.9-onbuild);避免在 ONBUILD 中使用 ADD/COPY,若构建上下文缺失目标资源会导致构建失败。