参考文章:Dockerfile构建镜像最佳实践
在前文Dockefile及命令详解中我们已经学习了如何通过Dockerfile构建镜像以及命令的详细说明,但是在生产环境或项目使用时如何构建出一个尽可能小的镜像是一个必须要学会的要点,本文将带领大家讨论如何精简镜像以及精简镜像带来的好处。在学习本文前建议大家看下Docker核心技术原理和Docker容器和镜像的区别文章中关于镜像的分层等知识有基础的了解。
一、为什么要精简镜像
Docker镜像由很多镜像层(Layers)组成(最多127层),镜像层依赖于一系列的底层技术,比如文件系统(filesystems)、写时复制(copy-on-write)、联合挂载(union mounts)等技术,总的来说,Dockerfile中的每条指令都会创建一个镜像层,继而会增加整体镜像的尺寸。
下面是精简Docker镜像尺寸的好处:
1、减少构建时间
2、减少磁盘使用量
3、减少下载时间
4、因为包含文件少,攻击面减小,提高了安全性
5、提高部署速度
二、如何精简镜像
2.1优化基础镜像
优化基础镜像的方法就是选用合适的更小的基础镜像,常用的Linux系统镜像一般有 Ubuntu、CentOs、Alpine,其中Alpine更推荐使用。大小对比如下:
lynzabo@ubuntu ~/s> docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEubuntu latest 74f8760a2a8b 8 days ago82.4MBalpine latest 11cd0b38bc3c 2 weeks ago 4.41MBcentos7 49f7960eb7e4 7 weeks ago 200MBdebian latest 3bbb526d2608 8 days ago101MBlynzabo@ubuntu ~/s>
Alpine是一个高度精简又包含了基本工具的轻量级Linux发行版,基础镜像只有4.41M,各开发语言和框架都有基于Alpine制作的基础镜像,强烈推荐使用它。Alpine镜像各个语言和框架支持情况,可以参考《优化Docker镜像、加速应用部署,教你6个小窍门》。
查看上面的镜像尺寸对比结果,你会发现最小的镜像也有4.41M,那么有办法构建更小的镜像吗?答案是肯定的,例如gcr.io/google_containers/pause-amd64:3.1
镜像仅有742KB。为什么这个镜像能这么小?在为大家解密之前,再推荐两个基础镜像:
1、scratch镜像
scratch是一个空镜像,只能用于构建其他镜像,比如你要运行一个包含所有依赖的二进制文件,如Golang程序,可以直接使用scratch作为基础镜像。现在给大家展示一下上文提到的Google pause镜像Dockerfile:
FROM scratchARG ARCHADD bin/pause-${ARCH} /pauseENTRYPOINT ["/pause"]
Google pause镜像使用了scratch作为基础镜像,这个镜像本身是不占空间的,使用它构建的镜像大小几乎和二进制文件本身一样大,所以镜像非常小。当然在我们的Golang程序中也会使用。对于一些Golang/C程序,可能会依赖一些动态库,你可以使用自动提取动态库工具,比如ldd、linuxdeployqt等提取所有动态库,然后将二进制文件和依赖动态库一起打包到镜像中。
2、busybox镜像
scratch是个空镜像,如果希望镜像里可以包含一些常用的Linux工具,busybox镜像是个不错选择,镜像本身只有1.16M,非常便于构建小镜像。
2.2 串联 Dockerfile 指令
大家在定义Dockerfile时,如果太多的使用RUN指令,经常会导致镜像有特别多的层,镜像很臃肿,而且甚至会碰到超出最大层数(127层)限制的问题,遵循 Dockerfile 最佳实践,我们应该把多个命令串联合并为一个 RUN(通过运算符&&
和/
来实现),每一个 RUN 要精心设计,确保安装构建最后进行清理,这样才可以降低镜像体积,以及最大化的利用构建缓存。
下面是一个优化前Dockerfile:
FROM ubuntuENV VER3.0.0 ENV TARBALL http://download.redis.io/releases/redis-$VER.tar.gz # ==> Install curl and helper tools...RUN apt-get update RUN apt-get install -y curl make gcc # ==> Download, compile, and install...RUN curl -L $TARBALL | tar zxv WORKDIR redis-$VER RUN make RUN make install #...# ==> Clean up...WORKDIR / RUN apt-get remove -y --auto-remove curl make gcc RUN apt-get clean RUN rm -rf /var/lib/apt/lists/* /redis-$VER #...CMD ["redis-server"]
构建镜像,名称叫test/test:0.1
。
我们对Dockerfile做优化,优化后Dockerfile:
FROM ubuntuENV VER3.0.0 ENV TARBALL http://download.redis.io/releases/redis-$VER.tar.gzRUN echo "==> Install curl and helper tools..." && \ apt-get update && \apt-get install -y curl make gcc && \echo "==> Download, compile, and install..." && \curl -L $TARBALL | tar zxv && \cd redis-$VER&& \make && \make install&& \echo "==> Clean up..." && \apt-get remove -y --auto-remove curl make gcc && \apt-get clean && \rm -rf /var/lib/apt/lists/* /redis-$VER#...CMD ["redis-server"]
构建镜像,名称叫test/test:0.2
。
对比两个镜像大小:
root@k8s-master:/tmp/iops# docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEtest/test 0.2 58468c0222ed 2 minutes ago 98.1MBtest/test 0.1 e496cf7243f2 6 minutes ago 307MBroot@k8s-master:/tmp/iops#
可以看到,将多条RUN命令串联起来构建的镜像大小是每条命令分别RUN的三分之一。
提示:为了应对镜像中存在太多镜像层,Docker 1.13版本以后,提供了一个压扁镜像功能,即将 Dockerfile 中所有的操作压缩为一层。这个特性还处于实验阶段,Docker默认没有开启,如果要开启,需要在启动Docker时添加-experimental 选项,并在Docker build 构建镜像时候添加 --squash 。我们不推荐使用这个办法,请在撰写 Dockerfile 时遵循最佳实践编写,不要试图用这种办法去压缩镜像。
2.3 多阶段构建
使用多阶段构建
Dockerfile中每条指令都会为镜像增加一个镜像层,并且你需要在移动到下一个镜像层之前清理不需要的组件。实际上,有一个Dockerfile用于开发(其中包含构建应用程序所需的所有内容)以及一个用于生产的瘦客户端,它只包含你的应用程序以及运行它所需的内容。这被称为“建造者模式”。Docker 17.05.0-ce版本以后支持多阶段构建。使用多阶段构建,你可以在Dockerfile中使用多个FROM
语句,每条FROM
指令可以使用不同的基础镜像,这样您可以选择性地将服务组件从一个阶段COPY
到另一个阶段,在最终镜像中只保留需要的内容。
下面是一个使用COPY --from
和FROM ... AS ...
的Dockerfile:
# CompileFROM golang:1.9.0WORKDIR /go/src/v9.git...com/.../k8s-monitorCOPY . .WORKDIR /go/src/v9.git...com/.../k8s-monitorRUN make buildRUN mv k8s-monitor /root# Package# Use scratch imageFROM scratchWORKDIR /root/COPY --from=0 /root .EXPOSE 8080CMD ["/root/k8s-monitor"]
构建镜像,你会发现生成的镜像只有上面COPY
指令指定的内容,镜像大小只有2M。这样在以前使用两个Dockerfile(一个Dockerfile用于开发和一个用于生产的瘦客户端),现在使用多阶段构建就可以搞定。
它是如何工作的?第二个FROM指令以alpine:latest image为基础开始一个新的构建阶段。COPY –from = 0行仅将前一阶段的构建文件复制到此新阶段。Go SDK和任何中间层都被遗忘,而不是保存在最终image中。
为多阶段构建命名
默认情况下,阶段未命名,您可以通过整数来引用它们,从第0个FROM指令开始。但是,您可以通过向FROM指令添加as NAME来命名您的阶段。此示例通过命名阶段并使用COPY指令中的名称来改进前一个示例。这意味着即使稍后重新排序Dockerfile中的指令,COPY也不会中断。
# CompileFROM golang:1.9.0 AS builderWORKDIR /go/src/v9.git...com/.../k8s-monitorCOPY . .WORKDIR /go/src/v9.git...com/.../k8s-monitorRUN make buildRUN mv k8s-monitor /root# Package# Use scratch imageFROM scratchWORKDIR /root/COPY --from=builder /root .EXPOSE 8080CMD ["/root/k8s-monitor"]
停在特定构建阶段
构建映像时,不一定需要构建整个Dockerfile每个阶段。您可以指定目标构建阶段。以下命令假定您使用的是以前的Dockerfile,但在名为builder的阶段停止:
$ docker build --target builder -t alexellis2/href-counter:latest .
使用此功能可能的一些非常适合的场景是:
调试特定的构建阶段
在debug阶段,启用所有调试或工具,而在production阶段尽量精简
在testing阶段,您的应用程序将填充测试数据,但在production阶段则使用生产数据
使用外部镜像作为stage
使用多阶段构建时,您不仅可以从Dockerfile中创建的镜像中进行复制。您还可以使用COPY –from指令从单独的image中复制,使用本地image名称,本地或Docker注册表中可用的标记或标记ID。如有必要,Docker会提取image并从那里开始复制。语法是:
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
三、其他优化镜像构建方法
3.1利用分层机制,减小镜像传输大小
Docker在build镜像的时候,如果某个命令相关的内容没有变化,会使用上一次缓存(cache)的文件层,在构建业务镜像的时候可以注意下面两点:
不变或者变化很少的体积较大的依赖库和经常修改的自有代码分开;
因为cache缓存在运行Docker build命令的本地机器上,建议固定使用某台机器来进行Docker build,以便利用cache。
下面是构建Spring Boot应用镜像的例子,用来说明如何分层。其他类型的应用,比如Java WAR包,Nodejs的npm 模块等,可以采取类似的方式。
1、在Dockerfile所在目录,解压缩maven生成的jar包
$ unzip <path-to-app-jar>.jar -d app
2、Dockerfile 我们把应用的内容分成4个部分COPY到镜像里面:其中前面3个基本不变,第4个是经常变化的自有代码。最后一行是解压缩后,启动spring boot应用的方式。
FROM openjdk:8-jre-alpineLABEL maintainer "opl-xws@"COPY app/BOOT-INF/lib/ /app/BOOT-INF/lib/COPY app/org /app/orgCOPY app/META-INF /app/META-INFCOPY app/BOOT-INF/classes /app/BOOT-INF/classesEXPOSE 8080CMD ["/usr/bin/java", "-cp", "/app", "org.springframework.boot.loader.JarLauncher"]
这样在构建镜像时候可大大提高构建速度。
3.2RUN命令中执行apt、apk或者yum类工具技巧
如果在RUN命令中执行apt、apk或者yum类工具,可以借助这些工具提供的一些小技巧来减少镜像层数量及镜像大小。举几个例子:
(1)在执行apt-get install -y
时增加选项— no-install-recommends
,可以不用安装建议性(非必须)的依赖,也可以在执行apk add
时添加选项--no-cache
达到同样效果;
(2)执行yum install -y
时候, 可以同时安装多个工具,比如yum install -y gcc gcc-c++ make ...
。将所有yum install
任务放在一条RUN命令上执行,从而减少镜像层的数量;
(3)组件的安装和清理要串联在一条指令里面,如apk --update add php7 && rm -rf /var/cache/apk/*
,因为Dockerfile的每条指令都会产生一个文件层,如果将apk add ...
和rm -rf ...
命令分开,清理无法减小apk命令产生的文件层的大小。 Ubuntu或Debian可以使用rm -rf /**var**/lib/apt/lists/*
清理镜像中缓存文件;CentOS等系统使用yum clean all
命令清理。
3.3压缩镜像
Docker 自带的一些命令还能协助压缩镜像,比如export
和import
$ docker run -d test/test:0.2$ docker export 747dc0e72d13 | docker import - test/test:0.3
使用这种方式需要先将容器运行起来,而且这个过程中会丢失镜像原有的一些信息,比如:导出端口,环境变量,默认指令。查看这两个镜像history信息,如下,可以看到test/test:0.3
丢失了所有的镜像层信息:
root@k8s-master:/tmp/iops# docker history test/test:0.3IMAGECREATED CREATED BYSIZECOMMENT6fb3f00b7a72 15 seconds ago84.7MB Imported from -root@k8s-master:/tmp/iops# docker history test/test:0.2IMAGECREATED CREATED BY SIZECOMMENT58468c0222ed 2 hours ago /bin/sh -c #(nop) CMD ["redis-server"] 0B 1af7ffe3d163 2 hours ago /bin/sh -c echo "==> Install curl and helper… 15.7MB 8bac6e733d54 2 hours ago /bin/sh -c #(nop) ENV TARBALL=http://downlo… 0B 793282f3ef7a 2 hours ago /bin/sh -c #(nop) ENV VER=3.0.00B 74f8760a2a8b 8 days ago/bin/sh -c #(nop) CMD ["/bin/bash"] 0B <missing> 8 days ago/bin/sh -c mkdir -p /run/systemd && echo 'do… 7B<missing> 8 days ago/bin/sh -c sed -i 's/^#\s*\(deb.*universe\)$… 2.76kB<missing> 8 days ago/bin/sh -c rm -rf /var/lib/apt/lists/*0B<missing> 8 days ago/bin/sh -c set -xe && echo '#!/bin/sh' > /… 745B <missing> 8 days ago/bin/sh -c #(nop) ADD file:5fabb77ea8d61e02d… 82.4MB root@k8s-master:/tmp/iops#
社区里还有很多压缩工具,比如Docker-squash ,用起来更简单方便,并且不会丢失原有镜像的自带信息,大家有兴趣可以试试。
3.4明确指定镜像版本,管理更方便
为了让版本管理起来更方便,应用部署速度更快,在创建镜像的过程中,建议工程师们明确指定包含版本或者其他辅助信息的tag。如果不指定镜像tag,默认会使用latest。每次启动应用实例时,都需要去镜像仓库检查镜像是否更新。这种方式不利于版本管理,对应用启动速度也有一定影响。
四、总结
使用小容器镜像的性能和安全优势不言而喻,使用小的基础镜像和builder pattern可以更容易地构建小镜像,并且还有许多其他技术可用于单个技术栈和编程语言,以最小化容器体积。 无论你做什么,你都可以确信你保持容器镜像最小化的努力是值得的!Docker镜像的精简手段和精简效果值得深入探讨和实践,希望本文能为大家带来帮助。
参考文献:
1.https://mp./s/T1Rp8x-WWzG9iXqXFp3ADw
2./a1010256340/article/details/80092038
3.
4.https://wilhelmguo.tk/blog/post/william/Docker构建之多阶段构建
5./x/page/t0752jh1emh.html
如果觉得《Dockerfile构建镜像最佳实践》对你有帮助,请点赞、收藏,并留下你的观点哦!