在构建Docker容器时,应该想办法获取更小的镜像,因为更小的镜像传输和部署速度更快。但是RUN语句总是创建一个新层,在生成图像之前需要使用很多中间文件。在这种情况下,如何获取更小的图像呢?您可能已经注意到,大多数Dockerfiles使用了一些奇怪的技巧:FROMubuntuRUNapt-getupdate&&apt-getinstallvim为什么要使用&&?而不是使用两个RUN语句?例如:FROMubuntuRUNapt-getupdateRUNapt-getinstallvim从Docker1.10开始,COPY、ADD和RUN语句向映像添加新层。前面的示例创建了两层而不是一层。镜像层就像Git提交。Docker的layer用于保存镜像上一版本和当前版本的差异。就像Git提交一样,如果您与其他存储库或镜像共享它们会很方便。实际上,当您从注册表中请求图像时,您只是在下载您尚未拥有的图层。这是一种非常有效的图像共享方式。但是额外的层不是免费的。图层仍然占用空间,图层越多,最终图像就越大。Git存储库在这方面也很相似,存储库的大小随着层数的增加而增加,因为Git必须保存提交之间的所有更改。在过去,将多个RUN语句组合在一个命令行中可能是一种很好的做法,如上面的第一个示例,但现在看来这样做并不合适。1.使用Docker多阶段构建将多个层压缩为一个当Git存储库变大时,您可以选择将历史提交压缩为单个提交。事实证明,在Docker中也可以使用多阶段构建来达到类似的目的。在此示例中,您将构建一个Node.js容器。让我们从index.js开始:constexpress=require('express')constapp=express()app.get('/',(req,res)=>res.send('HelloWorld!'))app.listen(3000,()=>{console.log(\`在端口3000上侦听的示例应用程序!\`)})和package.json:{"name":"hello-world","version":"1.0.0","main":"index.js","dependencies":{"express":"^4.16.2"},"scripts":{"start":"nodeindex.js"}}你可以使用以下Dockerfile来打包此应用程序:FROMnode:8EXPOSE3000WORKDIR/appCOPYpackage.jsonindex.js./RUNnpminstallCMD\["npm","start"\]并开始构建映像:$dockerbuild-tnode-香草。然后验证它是否正在使用:$dockerrun-p3000:3000-ti--rm--initnode-vanilla你应该能够访问http://localhost:3000并收到“HelloWorld!”。Dockerfile中使用了COPY语句和RUN语句,因此正如预期的那样,新图像应该比基础图像至少多两层:$dockerhistorynode-vanillaIMAGECREATEDBYCREATEDBY]??????????0Bbc8c3cc813ae???/bin/sh?-c?npm?install??????????????????????????2.91MBbac31afb6f42???/bin/sh?-c?#(nop)?COPY?multi:3071ddd474429e1…???364B500a9fbef90e???/bin/sh?-c?#(nop)?WORKDIR?/app??????????????????0B78b28027dfbf/bin/sh-c#(not)公开30000bb87c2ad8344d/bin/sh-c#(not)cmd\["node"\]0b<;missing>/bin/sh-cset-ex&&Ekeyin6A010...4.17MB/bin/sh-c#(nop)ENVYARN\_VERSION=1.3.20B/bin/sh-cARCH=&&dpkgArch="$(dpkg--print...56.9MB<缺失>;/bin/sh-c#(nop)ENVNODE\_VERSION=8.9.40B<缺失>/bin/sh-cset-ex&&为keyin94AE3...129kB/bin/sh-cgroupadd--gid1000node&&use...335kB/bin/sh-cset-ex;apt-getupdateglt;mapt-4>/bin/sh-capt-getupdate&&apt-getinstall...123MB<缺失>/bin/sh-cset-ex;如果!命令-vgpg>/...0B<缺失>/bin/sh-capt-getupdate&&apt-getinstall...44.6MB<缺失>/bin/sh-c#(nop)CMD\["bash"\]0B/bin/sh-c#(nop)ADDfile:1dd78a123212328bd...123MB但实际上,生成的镜像有五个新层:每个层对应Dockerfile中的一条语句现在,让我们试试Docker的多阶段构建。您可以继续使用与上面相同的Dockerfile,但现在调用它两次:FROMnode:8asbuildWORKDIR/appCOPYpackage.jsonindex.js./RUNnpminstallFROMnode:8COPY--from=build/app/EXPOSE3000CMD\["index.js"\]Dockerfile的第一部分创建了三个层,然后将它们合并并复制到第二阶段。在第二阶段,在镜子的顶部添加了两个额外的层,所以总共是三层。现在要验证。首先,构建一个容器:$DockerBuild-TNode-Multi-Stage。查看镜像历史:$DockerHistoryNode-Multi-StageimageCreatedbySize331B81A245B1/BIN/SH-C#(NOP)["InDex.js"""\]0bbdfc932314AF/BIN/SH-C#(NOP)Expose30000bf8992F62A6/Bin/SH-C#(NOP)复制目录:E2B57DFF89BE62F77...1.62MBB87C2AD/BIN/SH-C#(NOP)cmd\]0b<;缺少>/bin/sh-cset-ex&&FRkeyin6A010...4.17MB<缺少>/bin/sh-c#(not)envyarn\_version=1.3.20b<丢失&g/bin;sh-cARCH=&&dpkgArch="$(dpkg--print...56.9MB<缺失>/bin/sh-c#(nop)ENVNODE\_VERSION=8.9.40B<缺失>/bin/sh-cset-ex&&forkeyin94AE3...129kB/bin/sh-cgroupadd--gid1000node&&use...335kB/bin/sh-capt-getupdate&&apt-getinstall...123MB<缺失>/bin/sh-cset-ex;如果!命令-vgpg>/…0B<缺失>/bin/sh-capt-getupdate&&apt-getinstall…44.6MB<缺失>/bin/sh-c#(nop)CMD\["bash"\]0B/bin/sh-c#(nop)ADDfile:1dd78a123212328bd...123MB文件大小有变化吗?$码头图片|grepnode-node-multi-stage331b81a245b1678MBnode-vanilla075d229d3f48679MB最后一张图片(node-multi-stage)变小了尽管它已经是一个很小的应用程序,但您已经缩小了图片的大小。但是整个图像还是很大的!有什么办法可以让它变小吗?2.使用distroless把容器里不需要的东西全部去掉。此映像包含Node.js以及yarn、npm、bash和其他二进制文件。由于它也是基于Ubuntu的,因此您拥有一个完整的操作系统,其中包含所有小型二进制文件和实用程序。但是运行容器时不需要这些东西,你需要的只是Node.js。Docker容器应该只包含一个进程和运行该进程所需的最少文件数,您不需要整个操作系统。事实上,您可以删除除Node.js之外的所有内容。但是怎么办?幸运的是,Google为我们提供了distroless。以下是distroless存储库的描述:“distroless”映像仅包含应用程序及其运行时依赖项,没有包管理器、shell和您在标准Linux发行版中可以找到的任何其他程序。这正是您所需要的!您可以调整Dockerfile以利用新的基础映像,如下所示:FROMnode:8asbuildWORKDIR/appCOPYpackage.jsonindex.js./RUNnpminstallFROMgcr.io/distroless/nodejsCOPY--from=build/app/EXPOSE3000CMD\["index.js"\]你可以照常编译镜像:$dockerbuild-tnode-distroless。该图像应该可以正常工作。要验证它,请像这样运行容器:$dockerrun-p3000:3000-ti--rm--initnode-distroless现在可以访问http://localhost:3000页面。如果没有其他额外的二进制文件,图像会不会小得多?$码头图片|grepnode-distrolessnode-distroless7b4db3b7f1e576.7MB只有76.7MB!比之前的图片小600MB!但是在使用distroless时有一些事情需要注意。当容器正在运行时,如果要检查它,可以使用以下命令附加到正在运行的容器:$dockerexec-tibashattach到正在运行的容器并运行bash命令,如下所示与建立SSH会话相同。但distroless版本是原始操作系统的精简版本,没有额外的二进制文件,因此容器内没有外壳!如何在没有外壳的情况下附加到正在运行的容器?答案是,你不能。这既是坏消息也是好消息。这是个坏消息,因为您只能在容器内执行二进制文件。您可以运行的唯一二进制文件是Node.js:$dockerexec-tinode说这是个好消息,因为如果攻击者利用您的应用程序获得对容器的访问权限,它就不会造成那么大的破坏作为访问外壳。换句话说,更少的二进制文件意味着更小的大小和更好的安全性,但代价是痛苦的调试。也许您不应该在生产环境中附加和调试容器,而应该使用日志记录和监控。但是,如果您确实需要调试并希望保持小型化怎么办?3.小型Alpine基础镜像您可以使用Alpine基础镜像来替换distroless基础镜像。AlpineLinux是:一个基于musllibc和busybox的面向安全的轻量级Linux发行版。换句话说,它是一个更小、更安全的Linux发行版。但是你也不要想当然的认为他们说的是真的,我们看看它的镜像是不是小一些。首先修改Dockerfile以使用node:8-alpine:FROMnode:8asbuildWORKDIR/appCOPYpackage.jsonindex.js./RUNnpminstallFROMnode:8-alpineCOPY--from=build/app/EXPOSE3000CMD\["npm","start"\]使用以下命令构建镜像:$dockerbuild-tnode-alpine。现在您可以检查图像大小:$dockerimages|grepnode-alpinenode-alpineaa1f85f8e72469.7MB69.7MB!比无尘镜还小!我现在可以附加到正在运行的容器吗?让我们试试吧。让我们先启动容器:$dockerrun-p3000:3000-ti--rm--initnode-alpineExampleapp监听端口3000!您可以附加到正在运行的容器:$dockerexec-ti9d8e97e307d7bashOCIruntimeexecfailed:execfailed:container\_linux.go:296:startingcontainerprocesscaused"exec:\\"bash\\":executablefilenotfoundin$PATH":unknown似乎不起作用,但也许你可以使用shell?$dockerexec-ti9d8e97e307d7sh/#成功!现在您可以附加到正在运行的容器。看起来很有希望,但有一个问题。Alpine基础映像基于muslc——C语言的替代标准库,而大多数Linux发行版(如Ubuntu、Debian和CentOS)都基于glibc。两个库都应该实现相同的内核接口。但它们的目的不同:glibc更通用,速度更快;muslc使用更少的空间并注重安全性。编译应用程序时,大部分是针对特定的libc编译的。如果你想将它们与另一个libc一起使用,你必须重新编译它们。换句话说,基于Alpine基础镜像构建容器可能会导致意外行为,因为标准C库不同。您可能会注意到差异,尤其是在处理预编译的二进制文件(例如Node.jsC++扩展)时。例如,PhantomJS预构建包将不会在Alpine上运行。您应该选择哪个基本映像?您应该使用Alpine、distroless还是raw镜像?如果你在生产中运行容器并且更关心安全性,那么distroless镜像可能更合适。添加到Docker镜像的每个二进制文件都会给整个应用程序增加一些风险。在容器中只安装一个二进制文件可以降低整体风险。例如,如果攻击者能够利用在distroless上运行的应用程序中的漏洞,他们将无法在容器内使用shell,因为根本就没有shell!请注意,OWASP本身建议最小化攻击面。如果您只关心较小的图像尺寸,请考虑基于Alpine的图像。它们非常小,但代价是兼容性较差。Alpine使用了一个稍微不同的标准C库——muslc。您可能会不时遇到一些兼容性问题。原始基础图像非常适合测试和开发。它体积庞大,但提供与UbuntuWorkstation相同的体验。此外,您还可以访问操作系统的所有二进制文件。回顾一下每个镜像的大小:node:8681MBnode:8使用多阶段构建到678MBgcr.io/distroless/nodejs76.7MBnode:8-alpine69.7MB更多学习内容可以在码农到架构师的培训路径中找到