此前,在早些时候,我发表过Distroless与多阶段构建,其中介绍了简单的多阶段构建方式。阅读本文,你将跟快的了解多阶段构建带来的便利以及使用方法。
本文中主要介绍多阶段构建的方式,这种方式本身就可以节省一部分空间,对于如何缩减镜像大小的几种方式类文章总结将会在此后进行编写发布,那将是后面会发生的事情。我们暂且来看多阶段构建带给我们的便利性
为什么镜像会变大?
Docker就像一个版本控制系统。每次更改都会创建一个新图层。每次在Docker中运行新命令时,它都会创建一个新层。这就是为什么在Dockerfiles中你会看到链接的多个命令。下面的阶段代表一层:
RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py
&& python get-pip.py
&& pip install awscli
因此,让我们假设您的容器需要您下载源文件,并构建容器(通常采用静态编译语言)。您必须从容器中删除源文件,否则源将成为镜像的一部分,从而增加了大小。并且,您无法做到,rm -rf $source_files_directory
因为它只会创建一个新图层。
你怎么解决这个问题?我们将向您展示旧方式和更好的方式。
旧的方式:build
如果使用最早的叠加累计的方式进行构建,如果是这样,事实上你使用的是两个docker镜像
- 一个镜像来执行构建。
- 一个精简的镜像,用于传送第一个构建的结果,而不会在第一个镜像中产生构建链和工具的损失。
您可能已经发现保持图层小的唯一方法是:
- 在进入下一层之前不再需要清理工件。
- 使用shell技巧和其他逻辑来保持层尽可能小,同时确保每个层只有前一层所需的工件。
例如,以下是使用构建器模式的常见解决方案,用于调出Tomcat容器并在其上部署应用程序:
在甚至于,将常用软件打包在一个镜像中,这些软件包括:git ,jdk,mvn等。这样一来,镜像大小无限扩大,出现问题,排查也不是很方便,如下实例
FROM tomcat:9.0.16-jre8
MAINTAINER www.linuxea.com mark
WORKDIR /usr/src/project
RUN apt-get update
&& apt-get install -y git openjdk-8-jdk
&& git clone http://GITUSER:GITPASS@git.ds.com/mark/java.git
&& wget https://downloads.gradle.org/distributions/gradle-4.8.1-bin.zip
&& unzip gradle-4.8.1-bin.zip
&& PATH=$PATH:$PWD/gradle-4.8.1/bin
&& gradle prod
&& mv /usr/src/project/build/ROOT.war /usr/local/tomcat/webapps/ROOT.war
&& rm -rf /usr/src/project/java
COPY entrypoint.sh /
COPY tomcat_conf_prod/* /usr/local/tomcat/conf/
RUN chmod +x /entrypoint.sh
首先,我们进入/usr/src/project
目录(或者COPY指令将我们当前的代码添加到容器中)。然后我们需要安装openjdk
和 grande
,git
克隆代码,编译它并构建ROOT.war
。之后,我们将 ROOT.war
文件移动到Tomcat目录中,然后我们进行一些最终配置,以使用entrypoint.sh
脚本准备和启动容器。
但是这样的结果可能并不是我们想要的,容器本身看起来很不错,但是该镜像的大小(934MB)很大,尽管我们做了一些挽救措施,删除克隆的代码,甚至于可以删除git,但是最大的问题是jdk,因为这个镜像本身的大小是464M。
那么更好的办法,你可以使用alpine基础镜像包。我们继续往下看。
更妥善的方法:Docker Multi-Stage
自Docker版本17。05(2017年10月)起可用的Docker Multi-Stage将通过删除不再需要的库,依赖项,包等来减少容器的最终大小。该过程包括:
- 将构建分成不同的阶段,只保留每个阶段的最终结果并将其移动到下一个阶段。
- 在Dockerfile中使用多个FROM语句。每个FROM指令可以使用不同的基础,并且每个指令都开始构建的新阶段。
- 有选择地将工件从一个阶段复制到另一个阶段。
我们可以将最后一个Dockerfile片段转换为下一个:
我们修改了方式,不在容器中克隆代码,而是复制进容器分段执行
FROM openjdk:8
MAINTAINER www.linuxea.com mark
COPY . /usr/src/project
WORKDIR /usr/src/project
RUN wget https://downloads.gradle.org/distributions/gradle-4.8.1-bin.zip
&& unzip gradle-4.8.1-bin.zip
&& PATH=$PATH:$PWD/gradle-4.8.1/bin
&& gradle prod
FROM tomcat:9.0.16-jre8
MAINTAINER www.linuxea.com mark
COPY --from=0 /usr/src/project/build/ROOT.war /usr/local/tomcat/webapps/ROOT.war
COPY entrypoint.sh /
COPY tomcat_conf_prod/* /usr/local/tomcat/conf/
RUN chmod +x /entrypoint.sh
亦或者这样:
FROM maven:3-jdk-11
MAINTAINER www.linuxea.com mark
COPY linuxea /linuxea
RUN mvn -f /linuxea/pom.xml clean package
FROM marksugar/java:jdk1.8.0_131
MAINTAINER www.linuxea.com mark
COPY --from=build /linuxea /linuxea
EXPOSE 8086
CMD ["/linuxea/target/hello-world-0.0.6.jar"]
Docker Multi-Stage与使用Builder模式相比如何?
主要区别在于,使用Docker Multi-Stage
,我们在同一个Dockerfile
中构建了两个不同的镜像。第一个是基于openjdk
我们用它来编译我们的代码并生成ROOT.war
文件。当我们声明第二个基于Tomcat的镜像时,就会发生改变,我们使用--from=0
指令将ROOT.war
从第一个镜像复制到第二个镜像。这样做,我们放弃了Gradle(构建工具)用于编译我们的应用程序的所有依赖项,并且只保留最重要的东西,我们的ROOT.war
文件。
你是否在担心,这个--from=0
的问题?事实上这个名称是可以进行命名的 ,如下示例:
FROM golang:1.7.3 as builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
总结
- 不必局限于第一个镜像的大小,你可以将所有的操作都在里面运行,因为对于第一个镜像来讲,仅做加工,而后将输出输出到第二个镜像,而第二个镜像才是我们将会使用的镜像,而第二个镜像只有存在编译完成的包,仅用来进行run。从安全方面来讲,有一定的意义
- 你也可以在本地编写脚本执行打包,编译,而后在添加到镜像中,某些时候,脚本来完成似乎更加方便,仅对脚本高手来讲(并不建议)。
- 你的镜像也不一定要使用tomcat的官方包,你甚至可以用来修改成自定义的模样,如:alpine,更小的体积,在此之上安装附带一些能够更快,更高效前提下的操作等。
- 并且你不需要拘于某一种形式来做,只需要快捷提供一个小巧的,满足条件的镜像即可。
- 常用于ci/cd构建,在一些环境下,缓存对于构建有加速的作用
学习更多
学习如何使用Docker CLI命令,Dockerfile命令,使用这些命令可以帮助你更有效地使用Docker应用程序。查看Docker文档和我的其他帖子以了解更多信息。
- docker目录
- 白话容器