NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

一个优雅的 Dockerfile 是怎样炼成的?

  • 2020-05-16
  • 本文字数:2521 字

    阅读完需:约 8 分钟

一个优雅的Dockerfile是怎样炼成的?

前言

如今 GitHub 仓库中已经包含了成千上万的 Dockerfile,但并不是所有的 Dockerfile 都是高效的。本文将从减少构建时间、减小镜像体积、可维护性以及重复利用等方面来介绍 Dockerfile 的最佳实践,以此来帮助大家编写更优雅的 Dockerfile。如果你是 Docker 的初学者,恭喜你,这篇文章就是为你准备的。


本文使用一个基于 Maven 的 Java 项目作为示例,然后不断改进 Dockerfile 的写法,直到最后写出一个最优雅的 Dockerfile。中间的所有步骤都是为了说明某一方面的最佳实践。

减少构建时间

一个开发周期包括构建 Docker 镜像,更改代码,然后重新构建 Docker 镜像。在构建镜像的过程中,如果能够利用缓存,可以减少不必要的重复构建步骤。

构建顺序影响缓存的利用率


镜像的构建顺序很重要,当你向 Dockerfile 中添加文件,或者修改其中的某一行时,那一部分的缓存就会失效,该缓存的后续步骤都会中断,需要重新构建。 所以优化缓存的最佳方法是把不需要经常更改的行放到最前面,更改最频繁的行放到最后面。

只拷贝需要的文件,防止缓存溢出


当拷贝文件到镜像中时,尽量只拷贝需要的文件,切忌使用 COPY . 指令拷贝整个目录。如果被拷贝的文件内容发生了更改,缓存就会被破坏。在上面的示例中,镜像中只需要构建好的 jar 包,因此只需要拷贝这个文件就行了,这样即使其他不相关的文件发生了更改也不会影响缓存。

最小化可缓存的执行层


每一个 RUN 指令都会被看作是可缓存的执行单元。太多的 RUN 指令会增加镜像的层数,增大镜像体积,而将所有的命令都放到同一个 RUN 指令中又会破坏缓存,从而延缓开发周期。当使用包管理器安装软件时,一般都会先更新软件索引信息,然后再安装软件。推荐将更新索引和安装软件放在同一个 RUN 指令中,这样可以形成一个可缓存的执行单元,否则你可能会安装旧的软件包。

减小镜像体积

镜像的体积很重要,因为镜像越小,部署的速度更快,攻击范围越小。

删除不必要依赖


删除不必要的依赖,不要安装调试工具。如果实在需要调试工具,可以在容器运行之后再安装。某些包管理工具(如 apt)除了安装用户指定的包之外,还会安装推荐的包,这会无缘无故增加镜像的体积。apt 可以通过添加参数 -–no-install-recommends 来确保不会安装不需要的依赖项。如果确实需要某些依赖项,请在后面手动添加。

删除包管理工具的缓存


包管理工具会维护自己的缓存,这些缓存会保留在镜像文件中,推荐的处理方法是在每一个 RUN 指令的末尾删除缓存。如果你在下一条指令中删除缓存,不会减小镜像的体积。


当然了,还有其他更高级的方法可以用来减小镜像体积,如下文将会介绍的多阶段构建。接下来我们将探讨如何优化 Dockerfile 的可维护性、安全性和可重复性。

可维护性

尽量使用官方镜像


使用官方镜像可以节省大量的维护时间,因为官方镜像的所有安装步骤都使用了最佳实践。如果你有多个项目,可以共享这些镜像层,因为他们都可以使用相同的基础镜像。

使用更具体的标签


基础镜像尽量不要使用 latest 标签。虽然这很方便,但随着时间的推移,latest 镜像可能会发生重大变化。因此在 Dockerfile 中最好指定基础镜像的具体标签。我们使用 openjdk 作为示例,指定标签为 8。其他更多标签请查看官方仓库。

使用体积最小的基础镜像


基础镜像的标签风格不同,镜像体积就会不同。slim 风格的镜像是基于 Debian 发行版制作的,而 alpine 风格的镜像是基于体积更小的 Alpine Linux 发行版制作的。其中一个明显的区别是:Debian 使用的是 GNU 项目所实现的 C 语言标准库,而 Alpine 使用的是 Musl C 标准库,它被设计用来替代 GNU C 标准库(glibc)的替代品,用于嵌入式操作系统和移动设备。因此使用 Alpine 在某些情况下会遇到兼容性问题。 以 openjdk 为例,jre 风格的镜像只包含 Java 运行时,不包含 SDK,这么做也可以大大减少镜像体积。

重复利用

到目前为止,我们一直都在假设你的 jar 包是在主机上构建的,这还不是理想方案,因为没有充分利用容器提供的一致性环境。例如,如果你的 Java 应用依赖于某一个特定的操作系统的库,就可能会出现问题,因为环境不一致(具体取决于构建 jar 包的机器)。

在一致的环境中从源代码构建

源代码是你构建 Docker 镜像的最终来源,Dockerfile 里面只提供了构建步骤。



首先应该确定构建应用所需的所有依赖,本文的示例 Java 应用很简单,只需要 Maven 和 JDK,所以基础镜像应该选择官方的体积最小的 maven 镜像,该镜像也包含了 JDK。如果你需要安装更多依赖,可以在 RUN 指令中添加。pom.xml 文件和 src 文件夹需要被复制到镜像中,因为最后执行 mvn package 命令(-e 参数用来显示错误,-B 参数表示以非交互式的“批处理”模式运行)打包的时候会用到这些依赖文件。


虽然现在我们解决了环境不一致的问题,但还有另外一个问题: 每次代码更改之后,都要重新获取一遍 pom.xml 中描述的所有依赖项。 下面我们来解决这个问题。

在单独的步骤中获取依赖项


结合前面提到的缓存机制,我们可以让获取依赖项这一步变成可缓存单元,只要 pom.xml 文件的内容没有变化,无论代码如何更改,都不会破坏这一层的缓存。上图中两个 COPY 指令中间的 RUN 指令用来告诉 Maven 只获取依赖项。


现在又遇到了一个新问题:跟之前直接拷贝 jar 包相比,镜像体积变得更大了,因为它包含了很多运行应用时不需要的构建依赖项。

使用多阶段构建来删除构建时的依赖项


多阶段构建可以由多个 FROM 指令识别,每一个 FROM 语句表示一个新的构建阶段,阶段名称可以用 AS 参数指定。本例中指定第一阶段的名称为 builder,它可以被第二阶段直接引用。两个阶段环境一致,并且第一阶段包含所有构建依赖项。


第二阶段是构建最终镜像的最后阶段,它将包括应用运行时的所有必要条件,本例是基于 Alpine 的最小 JRE 镜像。上一个构建阶段虽然会有大量的缓存,但不会出现在第二阶段中。为了将构建好的 jar 包添加到最终的镜像中,可以使用 COPY --from=STAGE_NAME 指令,其中 STAGE_NAME 是上一构建阶段的名称。



多阶段构建是删除构建依赖的首选方案。


本文从在非一致性环境中构建体积较大的镜像开始优化,一直优化到在一致性环境中构建最小镜像,同时充分利用了缓存机制。


原文链接:

https://blog.docker.com/2019/07/intro-guide-to-dockerfile-best-practices/


2020-05-16 17:171038

评论

发布
暂无评论
发现更多内容

面试突击25:sleep和wait有什么区别?

王磊

java面试

鲲鹏DevKit & BoostKit直播解密:如何“做开发者的开发者”

科技热闻

盘一盘常见的6种索引失效情况

华为云开发者联盟

MySQL 索引 字符串 查询 索引失效

大厂偏爱的Agent技术究竟是个啥

捉虫大师

架构 agent

初级工程师建议收藏|企业级APIs安全实践指南

领创集团AdvanceGroup

第三个模块作业

achilles

UMEM:友盟统计自定义事件多应用一键同步 & 批处理工具

SamgeApp

Docker Vue 友盟助手 友盟自定义事件批处理 友盟统计

关于MVVM和MVC,面试看这篇就够了

山河已无恙

mvc 全栈 MVVM 2月月更

工作想法小计(2):2/14 - 2/18

非晓为骁

个人成长

如何打造一个能自动回复的钉钉机器人

老表

Python 机器人 Linxu 跟老表学云服务器

FIddler+Proxifer工具对windows PC客户端进行抓包

喀拉峻

黑客 网络安全

eBPF 完美搭档:连接云原生网络的 Cilium

火山引擎边缘云

边缘计算 ebpf 云原生网络 cllium

学生管理系统的架构文档

卡西毛豆静爸

「架构实战营」

基于CC2530设计的智能风扇

DS小龙哥

2月月更 智能风扇

十年所学,梦想终至,不负时光 | 《云端架构》新书首推发布,来自极度努力的吕校长

博文视点Broadview

CSS实现阮大佬博文的阅读进度功能

战场小包

CSS css3 前端 2月月更

CNCF 沙箱项目 OCM Placement 多集群调度指南

阿里巴巴云原生

阿里云 云原生 OCM Placement

Pulsar 职位广场 | 腾讯、华为云、虾皮、众安保险、StreamNative 等多个热招岗位

Apache Pulsar

开源 架构 云原生 招聘 Apache Pulsar

数据库读写分离如何保证主从一致性?

蜜糖的代码注释

MySQL 数据库 2月月更

云原生时代,如何保证容器镜像安全?

极狐GitLab

DevSecOps 镜像安全 极狐GitLab

Nginx跨域解决配置示例

nginx 跨域

超硬核攻略!《2022金融云原生落地实用指南》重磅发布(限时免费下载)

York

Python 中的数组哪去了?

宇宙之一粟

Python 数组 2月月更

用简单例子带你了解联合索引查询原理及生效规则

华为云开发者联盟

sql 索引 查询 联合索引

系统学习 TypeScript(二)——开发流程和语法规则

编程三昧

typescript 前端 2月月更

『The ShardingSphere Global Echo』Vol.4

SphereEx

数据库 开源 中间件 ShardingSphere SphereEx

学生管理系统的架构设计

凌波微步

「架构实战营」

好用不卡,这些插件和配置让你的 Webstorm 更牛逼!

前端下午茶

前端 工具 webstorm

存储新图谱:DNA存储的边界与天地

脑极体

[Python]介绍

謓泽

Python 2月月更

[Python]第一章(建议收藏)

謓泽

Python 2月月更

一个优雅的Dockerfile是怎样炼成的?_文化 & 方法_Rancher_InfoQ精选文章