本文将通过Node程序来展示如何优化Docker镜像(优化思路通用,不分程序),主要是解决镜像体积过大以及CI/CD镜像构建的速度。本文演示如何一步步优化Dockerfile文件,优化结果如下:大小从1.06G到73.4M构建速度从29.6秒到1.3秒(对比第二次构建速度)Node项目简单写了一个wechat-bot供自己使用,接下来我们就用这个项目来演示如何优化Docker镜像。下面是我刚开始写Docker的时候没有仔细研究过的一个Dockerfile。FROMnode:14.17.3#设置环境变量ENVNODE_ENV=productionENVAPP_PATH=/node/app#设置工作目录WORKDIR$APP_PATH#将当前目录下的所有文件复制到镜像的工作目录下。dockerignore指定的文件不会复制COPY.$APP_PATH#安装依赖RUNyarn#Expose端口EXPOSE4300CMDyarnstartbuildstartbuild后如下图,我的简单Node程序镜像有1G多,后面我们会逐步优化减少这个尺寸。优化前言在优化之前,有一些事情我们必须了解。解决问题的第一步是找出问题的原因。Dockerfile文件中包含一条一条的指令,每条指令构建一层,因此每条指令的内容描述了如何构建该层。Docker镜像不仅仅是一个文件,而是一堆文件。最重要的文件是图层。构建镜像时,会逐层构建。上一层是下一层的基础。变化会再次发生,后一层的任何变化只会发生在这一层。例如,删除上一层文件的操作,实际上并没有删除上一层文件,只是将当前层文件标记为已删除。最终容器运行的时候,虽然看不到这个文件,但实际上这个文件会一直跟在镜像后面。镜像层会被缓存起来重复使用(这就是为什么从第二次开始构建镜像时速度会更快的原因,优化镜像构建速度的原理也是利用缓存原理来完成的)Dockerfile被修改,操作的文件变量发生变化,或者构建镜像时指定的变量不同,对应的镜像层缓存会失效。dockerbuild缓存机制,docker如何知道文件变化?Docker采用的策略是:获取Dockerfile下的内容(包括inode信息的一部分),计算一个唯一的hash值,如果hash值没有变化,可以认为文件内容没有变化,并且可以使用缓存机制,反之亦然。某一层的图片缓存失效后,后续的图片层缓存也会失效。镜像的每一层只记录文件变化。当容器启动时,Docker会计算镜像的每一层,最后生成一个文件系统。当我知道这一点时,我突然意识到,我们使用的操作系统,如Android、iOS、Windows、macOS等,其实就是一个文件系统,而我们的软件界面交互等,其实就是读写文件。我们在网页上写一个弹框,操作dom就是读写本地文件或者读写内存中的数据。不知道对不对。我不是具有专业背景的前端编码员。参考资料:https://www.cnblogs.com/handwr....htmlok,我们已经知道镜像是由多层文件系统组成的。如果我们想优化它的大小,我们需要减少层数,并且每层尽可能只包含本层需要的东西,任何多余的东西都应该在层构建结束之前清理掉,正文开始以下。优化Dockerfile优化第一层FROMnode:14.17.3方案一:使用Alpine版本的Node这也是大多数人都知道的优化镜像的方法。Alpine是一个非常小的Linux发行版。只要选择Alpine版本的Node,就会有很大的提升。我们把这句话改成命令,改成FROMnode:14.17.4-alpine(大家可以去Dockerhub看看Node有哪些版本标签)。据说效果显着。也可以用其他基础的小镜像,比如mhart/alpine-node,这个可以小一点,改成FROMmhart/alpine-node:14.17.3再试试,可以看到小了5M,虽然不是多,但秉承着能榨一点,积少成多的“老板原则”,把它榨到极致。方案二:使用纯Alpine镜像手动安装Node由于Alpine是最小的Linux,我们尝试使用纯Alpine镜像,自己安装Node。FROMalpine:latest#使用apk命令安装nodejs和yarn。如果使用npm启动,则不需要安装yarnRUNapkadd--no-cache--updatenodejs=14.17.4-r0yarn=1.22.10-r0#...下面的步骤不是改build后,看下图只有174M,小了很多。结论是,如果懒得追求极致,就用方案二,从1.06G降到174M。减少层数和不经常变化的层数。如前所述,ENV命令可以一次设置多个环境变量。如果命令可以执行一次,则不需要执行两次。再多一个命令将再添加一层。EXPOSE命令是一个暴露的端口。也可以不写这个命令,在启动容器的时候自己映射端口。如果写这个命令,因为端口不经常变化,所以提前写好这个命令。写这个命令有两个好处:帮助镜像用户理解这个镜像服务的守护端口,方便配置映射。在运行时使用随机端口映射时,即dockerrun-P时,会自动随机映射EXPOSE的端口。至于写不写,就看个人了。因为我会在项目启动命令中指定项目端口,只是在启动容器的时候映射一下,所以要维护一个地方,如果Dockerfile也写了,项目端口变了,需要在这里修改,这将增加维护成本。当然也有办法从配置文件中获取两边的端口变量,改配置文件即可。下面是重写的Dockerfile。FROMalpine:latest#使用apk命令安装nodejs和yarn。如果使用npm启动,则不需要安装yarnRUNapkadd--no-cache--updatenodejs=14.17.4-r0yarn=1.22.10-r0#ExposeportEXPOSE4300#SetenvironmentvariablesENVNODE_ENV=production\APP_PATH=/node/app#设置工作目录WORKDIR$APP_PATH#将当前目录下的所有文件复制到镜像的工作目录下。dockerignore指定的文件不会复制COPY.$APP_PATH#安装依赖RUNyarn#Start为了优化CMDyarnstart这一步,从镜像的大小或者构建镜像的速度上看不出明显的区别,因为改变图层的内容很小(没有反映出来),但是可以看出图像的图层缩小了,你可以自己试试看镜像图层。减少镜像层数是“好老板”为了防止“员工”浪费资源的传统好习惯。package.json提前提高了编译速度。从下图中我们可以看出,每次构建最耗时的就是执行yarn命令安装依赖的时候。大多数时候我们只是更改代码而依赖项保持不变。这时候,如果我们能够让这一步被缓存起来,在依赖没有变化的情况下,就不需要重新安装依赖,这样可以大大提高编译速度。前面我们说过,在构建镜像的时候,是一层层构建的,上一层是下一层的基础。如果是这种情况,我们会提前单独拷贝package.json文件到镜像中,然后在下一步安装依赖,执行命令安装依赖层的上一层就是拷贝package.json文件,因为安装依赖的命令是不会变的,所以只要package.json文件不变,就不会重新执行yarn安装依赖,会复用之前安装的依赖,原理讲清楚了,咱们看下面的效果。修改后的Dockerfile文件:FROMalpine:latest#使用apk命令安装nodejs和yarn。如果使用npm启动,则不需要安装yarnRUNapkadd--no-cache--updatenodejs=14.17.4-r0yarn=1.22.10-r0#ExposedPortEXPOSE4300#设置环境变量ENVNODE_ENV=production\APP_PATH=/node/app#设置工作目录WORKDIR$APP_PATH#复制package.json到工作目录COPYpackage.json。#安装依赖RUNyarn#复制当前目录所有文件到镜像的工作目录。dockerignore指定的文件不会被复制。COPY..#startcommandCMDyarnstartbuild看下图,编译时间从29.6s到1.3s,使用缓存的层前面会有一个CACHED字样,仔细看下图可以看到。充分利用Docker的缓存功能是优化构建速度的好方法。使用多阶段构建再次压缩图像的大小。多阶段构建这里就不多说了。不知道的可以先搜索相关资料。因为我们在运行Node程序的时候,只需要产生的依赖和Node能够运行的最终文件,也就是说我们只需要package.js文件中的dependencies中的依赖来运行项目,而devDependencies依赖项仅在编译阶段使用。项目运行时不使用。例如,我们的项目是用typescript编写的。Node无法直接运行ts文件。ts文件需要编译成js文件。要运行项目,我们只需要编译后的文件和dependencies中的依赖项。可以运行,就是说最终的镜像只需要我们需要的,其他的什么都可以删掉。接下来,我们使用Dockerfile的多阶段重写。#构建基础镜像FROMalpine:3.14ASbase#设置环境变量ENVNODE_ENV=production\APP_PATH=/node/app#设置工作目录WORKDIR$APP_PATH#安装nodejs和yarnRUNapkadd--no-cache--updatenodejs=14.17.4-r0yarn=1.22.10-r0#使用基础镜像安装依赖阶段FROMbaseASinstall#复制package.json到工作目录COPYpackage.json./#安装依赖RUNyarn#最后阶段,也就是这个阶段构建输出镜像,而前面的阶段是为这个阶段做铺垫FROMbase#将依赖阶段生成的node_modules文件夹复制到工作目录COPY--from=install$APP_PATH/node_modules./node_modules#当前目录下的所有文件(除了.dockerignore排除的路径),全部复制到镜像的工作目录下COPY..#StartCMDyarnstart细心的朋友会发现我这里指定了Alpine版本,上面用的是最新版本,因为有一个刚才需要注意的坑,我t是我们在选择Alpine版本的时候,最好不要选择最新版本,因为后面要安装的软件版本在最新版本的Alpine中可能没有对应的软件版本号,会出现安装错误。我刚把车翻了点击查看Alpine版本包信息。构建完成后,让我们看看图像的大小。上次是174M,再次降到73.4M,挤压到极点。镜像:“放开我,我真的没有。”说明:我把这次构建分为三个阶段:第一阶段:构建基础镜像、安装依赖、编译、运行等,也就是把所有阶段共享的东西封装在一个基础镜像中,供其他阶段使用第一阶段,比如设置环境变量,设置工作目录,安装nodejs,yarn等第二阶段:安装依赖阶段在这个阶段,安装依赖。如果项目需要编译,可以在这个阶段安装依赖并编译。这里我说的是下载依赖的小细节,就是执行yarn--production并添加一个production参数或者环境变量NODE_ENV作为production,yarn不会安装devDependencies中列出的任何软件包,点我查看官方文档中,因为我设置了环境变量,所以没有加这个参数。第三阶段:最后使用镜像复制第二阶段安装好的依赖文件夹,然后将代码文件复制到工作目录,执行启动命令,安装第二阶段额外的依赖一些垃圾我们不要need,我们只需要复制需要用到的,大大减小了镜像的体积。如果项目需要编译,复制编译后的文件夹后,编译前的代码不需要再复制,编译后的代码和依赖就可以运行项目了。在多阶段构建中,最终生成的图像只能是最后一个阶段的结果。但是,前一阶段的文件可以复制到后一阶段,这就是多阶段构建的最大意义。最终优化结果:大小从1.06G到73.4M,构建速度从29.6秒到1.3秒(对比第二次构建的速度)。至此,压缩图片的手段就结束了。如果各位大佬还有压榨的手段,可以分享一下。镜像内心独白:“有礼貌吗?回来”GitHub建镜像的动作问题GitHub提供的动作每次都是干净的实例。这是什么意思?每次执行的时候都是干净的机器,会导致一个问题,导致Docker无法使用缓存。有解决办法吗?我想到了三种解决方案:1、我使用的是Docker官方提供的action缓存方案Github缓存方案。2.self-hostedactionsrunningmachine相当于GitLab的runner。如果你自己提供转轮,你提供的机器不会每次都是干净的机器。具体参见actions官方文档。3.先构建一个安装了依赖包的镜像,然后基于这个镜像再次构建,相当于多阶段构建。将前两个阶段构建的镜像产品推送到镜像仓库,然后以这个镜像为基础构建后续部分。使用镜像仓库存放基础镜像,达到缓存的效果。#建立在这个图像的基础上。该镜像为项目依赖并推送到镜像仓库的镜像。这里是从镜像仓库拉取的。FROMproject-base-image:latestCOPY..CMDyarnstart参考:https://evilmartians.com/chron...ching最后的项目仓库地址:https://github.com/iamobj/wechat-bot如果文章有错误,请指正,以免误导人。
