2023年7月6日发(作者:)
三个技巧,将Docker镜像体积减⼩90%在构建 Docker 容器时,应该尽量想办法获得体积更⼩的镜像,因为传输和部署体积较⼩的镜像速度更快。但RUN语句总是会创建⼀个新层,⽽且在⽣成镜像之前还需要使⽤很多中间⽂件,在这种情况下,该如何获得体积更⼩的镜像呢?你可能已经注意到了,⼤多数 Dockerfiles 都使⽤了⼀些奇怪的技巧:FROM ubuntuRUN apt-get update && apt-get install vim为什么使⽤ &&?⽽不是使⽤两个 RUN 语句代替呢?⽐如:FROM ubuntuRUN apt-get updateRUN apt-get install vim从 Docker 1.10 开始,COPY、ADD和RUN语句会向镜像中添加新层。前⾯的⽰例创建了两个层⽽不是⼀个。镜像的层就像 Git 的提交(commit)⼀样。Docker 的层⽤于保存镜像的上⼀版本和当前版本之间的差异。就像 Git 的提交⼀样,如果你与其他存储库或镜像共享它们,就会很⽅便。实际上,当你向注册表请求镜像时,只是下载你尚未拥有的层。这是⼀种⾮常⾼效地共享镜像的⽅式。但额外的层并不是没有代价的。层仍然会占⽤空间,你拥有的层越多,最终的镜像就越⼤。Git 存储库在这⽅⾯也是类似的,存储库的⼤⼩随着层数的增加⽽增加,因为Git 必须保存提交之间的所有变更。过去,将多个RUN语句组合在⼀⾏命令中或许是⼀种很好的做法,就像上⾯的第⼀个例⼦那样,但在现在看来,这样做并不妥。通过Docker 多阶段构建将多个层压缩为⼀个当 Git 存储库变⼤时,你可以选择将历史提交记录压缩为单个提交。事实证明,在 Docker 中也可以使⽤多阶段构建达到类似的⽬的。在这个⽰例中,你将构建⼀个 容器。让我们从 开始:const express = require('express')const app = express()('/', (req, res) => ('Hello World!'))(3000, () => { (`Example app listening on port 3000!`)})和 :{ "name": "hello-world", "version": "1.0.0", "main": "", "dependencies": { "express": "^4.16.2" }, "scripts": { "start": "node " }}你可以使⽤下⾯的 Dockerfile 来打包这个应⽤程序:FROM node:8EXPOSE 3000WORKDIR /appCOPY ./RUN npm installCMD ["npm", "start"]
FROM node:10MAINTAINER xialeistudio xialeistudio@KDIR /usr/src/appENV TZ Asia/ShanghaiARG registry= disturl=/distRUN yarn config set disturl $disturlRUN yarn config set registry $registryCOPY /usr/src/app/RUN yarn --frozen-lockfile --productionCOPY . /usr/src/appEXPOSE 8080CMD [ "yarn", "start:prod" ]然后开始构建镜像:$ docker build -t node-vanilla .然后⽤以下⽅法验证它是否可以正常运⾏:$ docker run -p 3000:3000 -ti --rm --init node-vanillaDockerfile 中使⽤了⼀个 COPY 语句和⼀个 RUN 语句,所以按照预期,新镜像应该⽐基础镜像多出⾄少两个层:$ docker history node-vanillaIMAGE CREATED BY SIZE075d229d3f48 /bin/sh -c #(nop) CMD ["npm" "start"] 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 #(nop) EXPOSE 3000 0Bb87c2ad8344d /bin/sh -c #(nop) CMD ["node"] 0B /bin/sh -c set -ex && for key in 6A010… 4.17MB /bin/sh -c #(nop) ENV YARN_VERSION=1.3.2 0B /bin/sh -c ARCH= && dpkgArch="$(dpkg --print… 56.9MB /bin/sh -c #(nop) ENV NODE_VERSION=8.9.4 0B /bin/sh -c set -ex && for key in 94AE3… 129kB /bin/sh -c groupadd --gid 1000 node && use… 335kB /bin/sh -c set -ex; apt-get update; apt-ge… 324MB /bin/sh -c apt-get update && apt-get install… 123MB /bin/sh -c set -ex; if ! command -v gpg > /… 0B /bin/sh -c apt-get update && apt-get install… 44.6MB /bin/sh -c #(nop) CMD ["bash"] 0B /bin/sh -c #(nop) ADD file:1dd78a123212328bd… 123MB但实际上,⽣成的镜像多了五个新层:每⼀个层对应 Dockerfile ⾥的⼀个语句。现在,让我们来试试 Docker 的多阶段构建。你可以继续使⽤与上⾯相同的 Dockerfile,只是现在要调⽤两次:FROM node:8 as buildWORKDIR /appCOPY ./RUN npm installFROM node:8COPY --from=build /app /EXPOSE 3000CMD [""]Dockerfile 的第⼀部分创建了三个层,然后这些层被合并并复制到第⼆个阶段。在第⼆阶段,镜像顶部⼜添加了额外的两个层,所以总共是三个层。
现在来验证⼀下。⾸先,构建容器:$ docker build -t node-multi-stage .查看镜像的历史:$ docker history node-multi-stageIMAGE CREATED BY SIZE331b81a245b1 /bin/sh -c #(nop) CMD [""] 0Bbdfc932314af /bin/sh -c #(nop) EXPOSE 3000 0Bf8992f6c62a6 /bin/sh -c #(nop) COPY dir:e2b57dff89be62f77… 1.62MBb87c2ad8344d /bin/sh -c #(nop) CMD ["node"] 0B /bin/sh -c set -ex && for key in 6A010… 4.17MB /bin/sh -c #(nop) ENV YARN_VERSION=1.3.2 0B /bin/sh -c ARCH= && dpkgArch="$(dpkg --print… 56.9MB /bin/sh -c #(nop) ENV NODE_VERSION=8.9.4 0B /bin/sh -c set -ex && for key in 94AE3… 129kB /bin/sh -c groupadd --gid 1000 node && use… 335kB /bin/sh -c set -ex; apt-get update; apt-ge… 324MB /bin/sh -c apt-get update && apt-get install… 123MB /bin/sh -c set -ex; if ! command -v gpg > /… 0B /bin/sh -c apt-get update && apt-get install… 44.6MB /bin/sh -c #(nop) CMD ["bash"] 0B /bin/sh -c #(nop) ADD file:1dd78a123212328bd… 123MB⽂件⼤⼩是否已发⽣改变?$ docker images | grep node-node-multi-stage 331b81a245b1 678MBnode-vanilla 075d229d3f48 679MB最后⼀个镜像(node-multi-stage)更⼩⼀些。你已经将镜像的体积减⼩了,即使它已经是⼀个很⼩的应⽤程序。但整个镜像仍然很⼤!有什么办法可以让它变得更⼩吗?⽤ distroless 去除不必要的东西这个镜像包含了 以及 yarn、npm、bash 和其他的⼆进制⽂件。因为它也是基于 Ubuntu 的,所以你等于拥有了⼀个完整的操作系统,其中包括所有的⼩型⼆进制⽂件和实⽤程序。但在运⾏容器时是不需要这些东西的,你需要的只是 。Docker 容器应该只包含⼀个进程以及⽤于运⾏这个进程所需的最少的⽂件,你不需要整个操作系统。实际上,你可以删除 之外的所有内容。但要怎么做?以下是 distroless 存储库的描述:“distroless”镜像只包含应⽤程序及其运⾏时依赖项,不包含程序包管理器、shell 以及在标准 Linux 发⾏版中可以找到的任何其他程序。这正是你所需要的!你可以对 Dockerfile 进⾏调整,以利⽤新的基础镜像,如下所⽰:FROM node:8 as buildWORKDIR /appCOPY ./RUN npm installFROM /distroless/nodejsCOPY --from=build /app /EXPOSE 3000CMD [""]你可以像往常⼀样编译镜像:$ docker build -t node-distroless .这个镜像应该能正常运⾏。要验证它,可以像这样运⾏容器:$ docker run -p 3000:3000 -ti --rm --init node-distroless不包含其他额外⼆进制⽂件的镜像是不是⼩多了?$ docker images | grep node-distrolessnode-distroless 7b4db3b7f1e5 76.7MB只有 76.7MB!⽐之前的镜像⼩了 600MB!但在使⽤ distroless 时有⼀些事项需要注意。当容器在运⾏时,如果你想要检查它,可以使⽤以下命令 attach 到正在运⾏的容器上:$ docker exec -ti bashattach 到正在运⾏的容器并运⾏ bash 命令就像是建⽴了⼀个 SSH 会话⼀样。但 distroless 版本是原始操作系统的精简版,没有了额外的⼆进制⽂件,所以容器⾥没有 shell!在没有 shell 的情况下,如何 attach 到正在运⾏的容器呢?答案是,你做不到。这既是个坏消息,也是个好消息。之所以说是坏消息,因为你只能在容器中执⾏⼆进制⽂件。你可以运⾏的唯⼀的⼆进制⽂件是 :$ docker exec -ti node说它是个好消息,是因为如果攻击者利⽤你的应⽤程序获得对容器的访问权限将⽆法像访问 shell 那样造成太多破坏。换句话说,更少的⼆进制⽂件意味着更⼩的体积和更⾼的安全性,不过这是以痛苦的调试为代价的。或许你不应在⽣产环境中 attach 和调试容器,⽽应该使⽤⽇志和监控。但如果你确实需要调试,⼜想保持⼩体积该怎么办?⼩体积的 Alpine 基础镜像你可以使⽤ Alpine 基础镜像替换 distroless 基础镜像。Alpine Linux 是:⼀个基于 musl libc 和 busybox 的⾯向安全的轻量级 Linux 发⾏版。换句话说,它是⼀个体积更⼩也更安全的 Linux 发⾏版。不过你不应该理所当然地认为他们声称的就⼀定是事实,让我们来看看它的镜像是否更⼩。先修改 Dockerfile,让它使⽤ node:8-alpine:FROM node:8 as buildWORKDIR /appCOPY ./RUN npm installFROM node:8-alpineCOPY --from=build /app /EXPOSE 3000CMD ["npm", "start"]使⽤下⾯的命令构建镜像:$ docker build -t node-alpine .现在可以检查⼀下镜像⼤⼩:$ docker images | grep node-alpinenode-alpine aa1f85f8e724 69.7MB69.7MB!甚⾄⽐ distrless 镜像还⼩!现在可以 attach 到正在运⾏的容器吗?让我们来试试。让我们先启动容器:$ docker run -p 3000:3000 -ti --rm --init node-alpineExample app listening on port 3000!你可以使⽤以下命令 attach 到运⾏中的容器:$ docker exec -ti 9d8e97e307d7 bashOCI runtime exec failed: exec failed: container_:296: starting container process caused "exec: "bash": executable file not found in $PATH": unknown看来不⾏,但或许可以使⽤ shell?$ docker exec -ti 9d8e97e307d7 sh / #成功了!现在可以 attach 到正在运⾏的容器中了。看起来很有希望,但还有⼀个问题。Alpine 基础镜像是基于 muslc 的——C 语⾔的⼀个替代标准库,⽽⼤多数 Linux 发⾏版如 Ubuntu、Debian 和 CentOS 都是基于 glibc的。这两个库应该实现相同的内核接⼝。但它们的⽬的是不⼀样的:glibc 更常见,速度也更快;muslc 使⽤较少的空间,并侧重于安全性。在编译应⽤程序时,⼤部分都是针对特定的 libc 进⾏编译的。如果你要将它们与另⼀个 libc ⼀起使⽤,则必须重新编译它们。换句话说,基于 Alpine 基础镜像构建容器可能会导致⾮预期的⾏为,因为标准 C 库是不⼀样的。你可能会注意到差异,特别是当你处理预编译的⼆进制⽂件(如 C++ 扩展)时。例如,PhantomJS 的预构建包就不能在 Alpine 上运⾏。你应该选择哪个基础镜像?你应该使⽤ Alpine、distroless 还是原始镜像?如果你是在⽣产环境中运⾏容器,并且更关⼼安全性,那么可能 distroless 镜像更合适。添加到 Docker 镜像的每个⼆进制⽂件都会给整个应⽤程序增加⼀定的风险。只在容器中安装⼀个⼆进制⽂件可以降低总体风险。例如,如果攻击者能够利⽤运⾏在 distroless 上的应⽤程序的漏洞,他们将⽆法在容器中使⽤ shell,因为那⾥根本就没有 shell!请注意,OWASP 本⾝就建议尽量减少攻击表⾯。如果你只关⼼更⼩的镜像体积,那么可以考虑基于 Alpine 的镜像。它们的体积⾮常⼩,但代价是兼容性较差。Alpine 使⽤了略微不同的标准 C 库——muslc。你可能会时不时地遇到⼀些兼容性问题。原始基础镜像⾮常适合⽤于测试和开发。它虽然体积很⼤,但提供了与 Ubuntu ⼯作站⼀样的体验。此外,你还可以访问操作系统的所有⼆进制⽂件。
发布者:admin,转转请注明出处:http://www.yc00.com/web/1688594549a153216.html
评论列表(0条)