
在创建Docker容器时,最好总是尽量减少图像的大小。 使用相同图层且重量更轻的图像-更快地传输和部署。
但是,当每次执行RUN
语句创建一个新层时,如何控制大小? 另外,在创建图像本身之前,您仍然需要中间工件...
您可能知道大多数Docker文件都有其自己的相当奇怪的功能,例如:
FROM ubuntu RUN apt-get update && apt-get install vim
好吧,为什么&&
在这里? 这样运行两个RUN
语句难道不是很容易吗?
FROM ubuntu RUN apt-get update RUN apt-get install vim
从Docker 1.10版本开始, COPY
, ADD
和RUN
操作符将新层添加到映像中。 在前面的示例中,创建了两层而不是一层。

git提交层。
Docker层保留了映像的先前版本与当前版本之间的差异。 与git commits一样,如果您与其他存储库或图像共享它们,它们将很方便。 实际上,当从注册表中请求图像时,只会加载缺少的图层,从而简化了容器之间图像的分离。
但是同时,每一层都会发生,并且层数越多,最终图像就越重。 Git存储库在这方面是相似的:存储库的大小随层数的增加而增加,因为它必须存储提交之间的所有更改。 与第一个示例一样,在同一行上合并多个RUN
语句曾经是一个好习惯。 但是现在,a,不。
1.使用分阶段组装的Docker映像将多层组合为一层
当Git存储库增长时,您可以简单地将整个变更历史总结为一个提交,而不必理会。 事实证明,可以通过分阶段组装在Docker中实现类似的功能。
让我们创建一个Node.js容器
让我们从index.js
开始:
const express = require('express') const app = express() app.get('/', (req, res) => res.send('Hello World!')) app.listen(3000, () => { console.log(`Example app listening on port 3000!`) })
和package.json
:
{ "name": "hello-world", "version": "1.0.0", "main": "index.js", "dependencies": { "express": "^4.16.2" }, "scripts": { "start": "node index.js" } }
使用以下Dockerfile
打包应用程序:
FROM node:8 EXPOSE 3000 WORKDIR /app COPY package.json index.js ./ RUN npm install CMD ["npm", "start"]
创建图像:
$ docker build -t node-vanilla .
检查一切正常:
$ docker run -p 3000:3000 -ti --rm --init node-vanilla
现在,您可以单击链接: http://本地主机:3000,然后在此处看到“ Hello World!”。
在Dockerfile
现在有了COPY
和RUN
运算符,因此与原始映像相比,我们将增长至少固定了两层:
$ docker history node-vanilla IMAGE CREATED BY SIZE 075d229d3f48 /bin/sh -c #(nop) CMD ["npm" "start"] 0B bc8c3cc813ae /bin/sh -c npm install 2.91MB bac31afb6f42 /bin/sh -c #(nop) COPY multi:3071ddd474429e1… 364B 500a9fbef90e /bin/sh -c #(nop) WORKDIR /app 0B 78b28027dfbf /bin/sh -c #(nop) EXPOSE 3000 0B b87c2ad8344d /bin/sh -c #(nop) CMD ["node"] 0B <missing> /bin/sh -c set -ex && for key in 6A010… 4.17MB <missing> /bin/sh -c #(nop) ENV YARN_VERSION=1.3.2 0B <missing> /bin/sh -c ARCH= && dpkgArch="$(dpkg --print… 56.9MB <missing> /bin/sh -c #(nop) ENV NODE_VERSION=8.9.4 0B <missing> /bin/sh -c set -ex && for key in 94AE3… 129kB <missing> /bin/sh -c groupadd --gid 1000 node && use… 335kB <missing> /bin/sh -c set -ex; apt-get update; apt-ge… 324MB <missing> /bin/sh -c apt-get update && apt-get install… 123MB <missing> /bin/sh -c set -ex; if ! command -v gpg > /… 0B <missing> /bin/sh -c apt-get update && apt-get install… 44.6MB <missing> /bin/sh -c #(nop) CMD ["bash"] 0B <missing> /bin/sh -c #(nop) ADD file:1dd78a123212328bd… 123MB
如您所见,最终映像增加了五个新层: Dockerfile
每个操作符Dockerfile
。 现在让我们尝试分阶段的Docker构建。 我们使用相同的Dockerfile
,包括两个部分:
FROM node:8 as build WORKDIR /app COPY package.json index.js ./ RUN npm install FROM node:8 COPY --from=build /app / EXPOSE 3000 CMD ["index.js"]
Dockerfile
的第一部分创建三层。 然后将各层合并并复制到第二阶段和最后阶段。 上面的图像中又添加了两层。 结果,我们分为三层。

让我们尝试一下。 首先,创建一个容器:
$ docker build -t node-multi-stage .
查看历史记录:
$ docker history node-multi-stage IMAGE CREATED BY SIZE 331b81a245b1 /bin/sh -c #(nop) CMD ["index.js"] 0B bdfc932314af /bin/sh -c #(nop) EXPOSE 3000 0B f8992f6c62a6 /bin/sh -c #(nop) COPY dir:e2b57dff89be62f77… 1.62MB b87c2ad8344d /bin/sh -c #(nop) CMD ["node"] 0B <missing> /bin/sh -c set -ex && for key in 6A010… 4.17MB <missing> /bin/sh -c #(nop) ENV YARN_VERSION=1.3.2 0B <missing> /bin/sh -c ARCH= && dpkgArch="$(dpkg --print… 56.9MB <missing> /bin/sh -c #(nop) ENV NODE_VERSION=8.9.4 0B <missing> /bin/sh -c set -ex && for key in 94AE3… 129kB <missing> /bin/sh -c groupadd --gid 1000 node && use… 335kB <missing> /bin/sh -c set -ex; apt-get update; apt-ge… 324MB <missing> /bin/sh -c apt-get update && apt-get install… 123MB <missing> /bin/sh -c set -ex; if ! command -v gpg > /… 0B <missing> /bin/sh -c apt-get update && apt-get install… 44.6MB <missing> /bin/sh -c #(nop) CMD ["bash"] 0B <missing> /bin/sh -c #(nop) ADD file:1dd78a123212328bd… 123MB
查看文件大小是否已更改:
$ docker images | grep node- node-multi-stage 331b81a245b1 678MB node-vanilla 075d229d3f48 679MB
是的,它已经变小了,但还不是很大。
2.我们使用distroless从容器中清除所有不必要的东西
当前图像为我们提供了Node.js, yarn
, npm
, bash
和许多其他有用的二进制文件。 而且,它基于Ubuntu。 因此,通过部署它,我们获得了具有许多有用的二进制文件和实用程序的成熟操作系统。
但是,我们不需要它们来运行容器。 唯一需要的依赖项是Node.js。
Docker容器必须支持一个进程的操作,并包含运行它的最少必需的一组工具。 不需要整个操作系统。
因此,除了Node.js,我们可以从中获得所有收益。
但是如何?
谷歌已经提出了类似的解决方案-GoogleCloudPlatform / distroless 。
存储库的描述为:
非发行版映像仅包含应用程序及其依赖项。 在标准Linux发行版中通常没有找到包管理器,shell或其他程序。
这就是您所需要的!
运行Dockerfile
以获取新映像:
FROM node:8 as build WORKDIR /app COPY package.json index.js ./ RUN npm install FROM gcr.io/distroless/nodejs COPY --from=build /app / EXPOSE 3000 CMD ["index.js"]
我们照常收集图像:
$ docker build -t node-distroless .
该应用程序应该可以正常工作。 要检查,请运行容器:
$ docker run -p 3000:3000 -ti --rm --init node-distroless
并转到http:// localhost:3000 。 没有额外的二进制文件,图像变得更容易了吗?
$ docker images | grep node-distroless node-distroless 7b4db3b7f1e5 76.7MB
就这样! 现在,它的重量仅为76.7 MB,少了600 MB!
一切都很酷,但是有一点很重要。 当容器运行时,需要检查它时,可以使用以下命令进行连接:
$ docker exec -ti <insert_docker_id> bash
连接到正在运行的容器并启动bash
与创建SSH会话非常相似。
但是由于distroless是原始操作系统的简化版本,因此没有其他二进制文件,也实际上没有shell!
如果没有外壳,如何连接到正在运行的容器?
最有趣的是什么都没有。
这不是很好,因为只能在容器中执行二进制文件。 唯一可以启动的是Node.js:
$ docker exec -ti <insert_docker_id> node
实际上,这样做有一个好处,因为如果某些攻击者可以访问该容器,则其危害要比其可以访问的Shell小得多。 换句话说,二进制文件更少-重量更轻,安全性更高。 但是,这是以更复杂的调试为代价的。
在这里应该注意,在产品环境中连接和调试容器是不值得的。 最好依靠正确配置的日志记录和监视系统。
但是,如果我们仍然需要调试,但又希望Docker映像最小,该怎么办?
3.使用Alpine减少基本图像
您可以用Alpine图像替换distroless。
Alpine Linux是基于musl libc和busybox的面向安全的轻量级发行版。 但是我们不会说一个字,而是要检查一下。
使用node:8-alpine
运行Dockerfile
:
FROM node:8 as build WORKDIR /app COPY package.json index.js ./ RUN npm install FROM node:8-alpine COPY --from=build /app / EXPOSE 3000 CMD ["npm", "start"]
创建图像:
$ docker build -t node-alpine .
检查尺寸:
$ docker images | grep node-alpine node-alpine aa1f85f8e724 69.7MB
在输出中,我们有69.7MB-甚至比一个发行版图像还小。
让我们检查是否可以连接到工作容器(对于distrolles映像,我们无法执行此操作)。
启动容器:
$ docker run -p 3000:3000 -ti --rm --init node-alpine Example app listening on port 3000!
并连接:
$ docker exec -ti 9d8e97e307d7 bash OCI runtime exec failed: exec failed: container_linux.go:296: starting container process caused "exec: \"bash\": executable file not found in $PATH": unknown
不成功。 但是,也许容器已经变硬了……:
$ docker exec -ti 9d8e97e307d7 sh / #
太好了! 我们设法连接到了容器,与此同时,它的图像也变小了。 但是这里有些细微差别。
高山映像基于muslc(C的替代标准库)。而大多数Linux发行版(如Ubuntu,Debian和CentOS)均基于glibc。 可以相信,这两个库都为使用内核提供了相同的接口。
但是,它们有不同的目标:glibc是最常见且最快速的,而muslc占用的空间更少,并且编写时带有安全性偏见。 通常,应用程序在编译时会编译为特定的C库,如果需要与其他库一起使用,则必须重新编译。
换句话说,在Alpine图像上构建容器可能导致意外事件,因为其中使用的标准C库是不同的。 当使用预编译的二进制文件(例如,C ++的Node.js扩展)时,这种区别将非常明显。
例如,完成的PhantomJS软件包在Alpine上不起作用。
那么要选择的基本图像是什么?
高山,平整或香草的外观-当然,最好根据情况决定。
如果您要处理产品并且安全性很重要,那么可能最好的方法是无坚不摧。
添加到Docker映像的每个二进制文件都会给整个应用程序的稳定性带来一定的风险。 通过在容器中仅安装一个二进制文件可以降低这种风险。
例如,如果攻击者可以在基于非发行版映像运行的应用程序中发现漏洞,则他无法在容器中运行该外壳,因为该外壳不存在!
如果出于某种原因,docker映像的大小对您来说非常重要,那么绝对值得仔细研究基于Alpine的映像。
它们确实很小,但是以兼容性为代价。 Alpine使用的标准C库muslc略有不同,因此有时会弹出问题。 可以在以下链接中找到示例: https : //github.com/grpc/grpc/issues/8528和https://github.com/grpc/grpc/issues/6126 。
香草图像是测试和开发的理想选择。
是的,它们很大,但它们在安装了Ubuntu的成熟计算机上看起来尽可能多。 此外,操作系统中的所有二进制文件均可用。
汇总收到的Docker映像的大小:
node:8
8681MB
node:8
,增量构建678MB
gcr.io/distroless/nodejs
76.7MB
node:8-alpine
69.7MB
译者的分词
阅读我们博客上的其他文章:
Kubernetes中的状态备份
备份大量异构Web项目
Redmine的电报机器人。 如何简化自己和他人的生活