如今各个公有镜像仓库中已经包含了成千上万的镜像文件,但并不是所有的镜像都是精简高效的。很多初学者刚开始都习惯使用FROM centos然后RUN 一堆yum install,这样还停留在虚拟机层面的使用,这样创建出来的镜像往往体积比较大。其实我们可以参考官方制作的dockerfile文件,或者以alpine版本的镜像为基础镜像进行定制,但是alpine初期的时候问题有点多,而且缺少很多软件包,不易进行排错,但现在很多常用的镜像都有alpine版本了,我们可以直接使用。本文将分享一些在工作中的具体使用经验,尽量以此来帮助大家编写出更精简更优雅的Dockerfile。
本文使用一个基于 Maven 的 Java 项目作为示例,然后不断优化 Dockerfile 的写法,直到最后写出一个比较合适的Dockerfile。中间的每一个步骤都是为了说明从某一方面的最佳实践。
一个开发周期大致包括代码开发,构建 Docker 镜像,代码测试,代码修改,然后重新构建 Docker 镜像。在构建镜像的过程中,如果能够利用缓存,可以减少不必要的重复构建步骤,大大加快制作镜像的速度。
[root@k8s-m1 docker1]# cat Dockerfile
FROM openjdk:7u131-jdk-alpine
RUN yum install vim ssh -y
ADD wg-kms-pm.jar /opt/wvmp/
### yum install vim ssh -y 放在add的前面更好
ENTRYPOINT ["java" "-Djava.security.egd=file:/dev/./urandom " "-jar" "/opt/wvmp/wg-kms-pm.jar"]
镜像的构建顺序很重要,当向 Dockerfile 中添加文件,或者修改其中的某一行时,那一部分的缓存就会失效,并且该缓存的后续所有操作都需要重新构建。所以优化缓存的最佳方法是把不需要经常更改的行放到最前面(如安装一些基础包),更改最频繁的行放到最后面(如从外部添加的jar业务包)。
[root@k8s-m1 docker1]# cat Dockerfile
FROM openjdk:7u131-jdk-alpine
RUN yum install vim ssh -y
###ADD . /opt/wvmp/ 由于文件夹中可能包含其他不需要的文件
ADD wg-kms-pm.jar /opt/wvmp/
ENTRYPOINT ["java" "-Djava.security.egd=file:/dev/./urandom " "-jar" "/opt/wvmp/wg-kms-pm.jar"]
当拷贝文件到镜像中时,尽量只拷贝需要的文件,最好不要使用COPY . 指令拷贝整个目录。因为如果被拷贝的文件夹中任意一个文件内容发生了更改,缓存就会被破坏。在上面的示例中,镜像中只需要构建好的 jar 包,因此只需要拷贝这个文件就行,这样即使其他不相关的文件发生了更改也不会影响缓存。
FROM openjdk:7u131-jdk-alpine
### RUN yum update
### RUN yum install vim ssh -y
RUN yum update && yum install vim ssh -y #将上面两行合并
### ADD adb_server-V1.2.0.12.tar.gz /opt/wvmp/
### ADD bootstrap.yml /opt/wvmp/
ADD bootstrap.yml wg-kms-pm.jar /opt/wvmp/ #将上面两个add合并,因为都是拷贝到同一文件夹下
ENTRYPOINT ["java" "-Djava.security.egd=file:/dev/./urandom " "-jar" "/opt/wvmp/wg-kms-pm.jar"]
通过前面的了解知道,docker镜像是分层的,每一个命令在制作镜像时都会生产一个新的镜像层。太多的命令行指令会增加镜像的层数,增大镜像体积,而如果能通过适当优化将某些命令放到同一行就能减少层数;但是如果将所有都放入同一行指令中,某一部分发生改变又会破坏缓存,影响构建速度。如当使用包管理器安装软件时,一般都会先更新软件索引信息,然后再安装软件。推荐将更新索引和安装软件放在同一个 RUN 指令中,这样可以形成一个可缓存的执行单元,否则你可能会安装旧的软件包。
镜像的体积很重要,因为镜像越小,部署的速度更快,攻击范围也越小。
在能满足使用,功能正常的情况下,尽量选择镜像大小比较小的作为基础镜像。如openjdk和jdk-alpine版本的大小从下图可以看出体积相差还是挺大的。
[root@k8s-m1 docker1]# cat Dockerfile
FROM openjdk:7u131-jdk-alpine
### RUN yum install vim ssh -y
##vim和ssh都不是运行java包的必须依赖包,可以不安装
ADD wg-kms-pm.jar /opt/wvmp/
ENTRYPOINT ["java" "-Djava.security.egd=file:/dev/./urandom " "-jar" "/opt/wvmp/wg-kms-pm.jar"]
删除不必要的依赖,不要安装调试工具。如果实在需要调试工具,可以在容器运行之后再安装或者只在测试镜像安装,调试完成后从新构建镜像。某些包管理工具(如 apt)除了安装用户指定的包之外,还会安装推荐的包,这会无缘无故增加镜像的体积。apt可以通过添加参数 -–no-install-recommends 来确保不会安装不需要的依赖项。如果确实需要某些依赖项,请在后面手动添加。
[root@k8s-m1 docker1]# cat Dockerfile
FROM openjdk:7u131-jdk-alpine
RUN yum install vim ssh -y && rm -rf /var/lib/yum/repos/
##增加删除缓存。但是yum默认没有缓存yum包,如果修改了配置需要删除
ADD wg-kms-pm.jar /opt/wvmp/
ENTRYPOINT ["java" "-Djava.security.egd=file:/dev/./urandom " "-jar" "/opt/wvmp/wg-kms-pm.jar"]
有时候,我们设置了包管理工具缓存安装的yum包,这些缓存会保留在镜像文件中,推荐的处理方法是在每一个 RUN 指令的末尾删除缓存(可以根据实际配置添加)。如果你在下一条指令中删除缓存,不会减小镜像的体积。
当然了,还有其他更高级的方法可以用来减小镜像体积,如下文将会介绍的多阶段构建。接下来我们将探讨如何优化 Dockerfile 的可维护性、安全性和可重复性。
官方的hubdocker仓库地址已经变成了https://hub-stage.docker.com/
使用官方镜像可以节省大量的维护时间,因为官方镜像的所有安装步骤都使用了最佳实践。如果你有多个项目,可以共享这些镜像层,因为他们都可以使用相同的基础镜像。
如openjdk,基础镜像尽量不要使用 latest 标签。虽然这很方便,但是latest 镜像随时可能发生更新而变得不适用,而且latest镜像比其他的alpine镜像体积大很多。因此在 Dockerfile 中最好指定基础镜像的具体标签。具体某个标签请参考官网提供的。
在功能满足的情况下,使用体积小的基础镜像。基础镜像的标签风格不同,镜像体积就会不同。slim 风格的镜像是基于 Debian 发行版制作的,而 alpine 风格的镜像是基于体积更小的 Alpine Linux 发行版制作的。其中一个明显的区别是:Debian 使用的是 GNU 项目所实现的 C 语言标准库,而 Alpine 使用的是 Musl C 标准库,它被设计用来替代 GNU C 标准库(glibc)的替代品,用于嵌入式操作系统和移动设备。因此使用 Alpine 在某些情况下会遇到兼容性问题。 以 openjdk 为例,jre 风格的镜像只包含 Java 运行时,不包含 SDK,这么做也可以大大减少镜像体积。
到目前为止,我们一直都在假设你的 jar 包是在某个服务器上提前编译好,但还不是最理想方案,因为没有充分利用容器提供的一致性环境。例如,如果Java 应用依赖于某一个特定的操作系统的库,有可能只在这一台服务器上正常编译(服务器配置了特殊环境),在其他环境编译运行就可能会出现问题,因为环境不一致(具体取决于构建 jar 包的机器)。
源代码是你构建 Docker 镜像的最终来源,Dockerfile 里面只提供了构建步骤,通过镜像容器提供一个一致的编译环境,就不用在本地编译好jar包。
[root@k8s-m1 docker1]# cat Dockerfile
##基础镜像为官方提供的,如果不适用,可以自行制作一个基础镜像
FROM maven:alpine
WORKDIR /opt/wvmp/
COPY pom.xml
COPY scr ./src
RUN mvn -e -B package
ENTRYPOINT ["java" "-Djava.security.egd=file:/dev/./urandom " "-jar" "/opt/wvmp/wg-kms-pm.jar"]
首先应该确定构建应用所需的所有依赖,本文的示例 Java 应用很简单,只需要 Maven 和 JDK,所以基础镜像应该选择官方的体积最小的 maven 镜像,该镜像也包含了 JDK。如果你需要安装更多依赖,可以通过RUN就在该镜像基础上安装好或者自行先制作好基础镜像。pom.xml文件和 src 文件夹需要被复制到镜像中,因为最后执行 mvn package 命令(-e 参数用来显示错误,-B 参数表示以非交互式的“批处理”模式运行)打包的时候会用到这些依赖文件。
通过使用docker直接提供编译环境我们解决了环境不一致的问题,但还有另外一个问题:每次代码更改之后,都要重新获取一遍 pom.xml 中描述的所有依赖项,这样就比较耗费时间。下面我们来解决这个问题。
[root@k8s-m1 docker1]# cat Dockerfile
##基础镜像为官方提供的,如果不适用,可以自行制作一个基础镜像
FROM maven:alpine
WORKDIR /opt/wvmp/
COPY pom.xml
RUN mvn -e -B package dependency:resolve
COPY scr ./src
RUN mvn -e -B package
ENTRYPOINT ["java" "-Djava.security.egd=file:/dev/./urandom " "-jar" "/opt/wvmp/wg-kms-pm.jar"]
结合前面提到的分层缓存机制,我们可以让获取依赖项这一步变成可缓存单元,只要 pom.xml 文件的内容没有变化,无论代码如何更改,都不会破坏这一层的缓存。上图中两个 COPY 指令中间的 RUN 指令用来告诉 Maven 只获取依赖项。
现在又出现一个新问题:跟之前直接拷贝 jar 包相比,镜像体积变得更大了,因为它包含了很多运行应用时不需要的构建依赖项。
[root@k8s-m1 docker1]# cat Dockerfile
##基础镜像为官方提供的,如果不适用,可以自行制作一个基础镜像
FROM maven:alpine AS mid-builder
WORKDIR /opt/wvmp/
COPY pom.xml
RUN mvn -e -B package dependency:resolve
COPY scr ./src
RUN mvn -e -B package
FROM openjdk:7u131-jdk-alpine
COPY --from=mid-builder /opt/wvmp/wg-kms-pm.jar /opt/wvmp/
ENTRYPOINT ["java" "-Djava.security.egd=file:/dev/./urandom " "-jar" "/opt/wvmp/wg-kms-pm.jar"]
多阶段构建可以由多个 FROM 指令识别,每一个 FROM 语句表示一个新的构建阶段,阶段名称可以用 AS 参数指定。本例中指定第一阶段的名称为 mid-builder,它可以被第二阶段直接引用。两个阶段环境一致,并且第一阶段包含所有构建依赖项。
第二阶段是构建最终镜像的最后阶段,它将包括应用运行时的所有必要条件,本例是基于 Alpine 的最小 JRE 镜像。上一个构建阶段虽然会有大量的缓存,但不会出现在第二阶段中。为了将构建好的 jar 包添加到最终的镜像中,可以使用 COPY --from=STAGE_NAME 指令,其中 STAGE_NAME 是上一构建阶段的名字。
多阶段构建是删除构建依赖的首选方案。
本文从多方面对镜像的制作过程进行优化,目的就是为了创建一个精简的镜像,提高效率,使用率。
更多关于docker容器和运维相关的知识,请前往博客主页。