在构建Docker容器时,您应该始终争取更小的镜像。因为共享层且尺寸较小的镜像可以更快地传输和部署。
但是,当每个RUN语句都创建一个新层,并且在镜像准备好之前需要中间层时,如何控制大小呢?
你可能已经注意到,大多数Dockerfiles都有一些奇怪的技巧,比如:
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提交一样,如果与其他存储库或镜像共享,它们也很方便。
事实上,当您从registry请求镜像时,只会下载尚未拥有的图层。这种方式更有效地共享镜像。
但分层也不是免费的
分层占用空间,分层越多,最终的镜像就越重。Git存储库在这方面是类似的。存储库的大小随着层数的增加而增加,因为Git必须存储提交之间的所有更改
在过去,将几个RUN语句组合在一行上是一种很好的做法。就像第一个例子一样。
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
> hello-world@1.0.0 start /app
> node index.js Example app listening on port 3000!
您应该能够访问http://localhost:3000 并看到Hello World!
Dockerfile中有一个COPY和一个RUN语句。所以你应该期望看到至少比基础镜像多两层:
相反,生成的镜像有五个新层: 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的第一部分创建了三个层。然后将镜像合并并复制到第二个也是最后一个阶段。另外两层添加到图像的顶部,总共3层。
然后运行命令构建
$ docker build -t node-multi-stage .
现在看看构建历史:
对比看看2次build的镜像的大小:
是的,最后一个镜像稍微小一点。
还不错!您减少了总体大小,即使这是一个已经精简的应用程序。
但镜像仍然很大!
你能做些什么让它更小吗?
2. 移除镜像当中不必要的部分
目前的版本附带了Node.js以及yarn、npm、bash和许多其他二进制文件。它也是基于Ubuntu的。这样,您就有了一个完全成熟的操作系统,以及它所有的二进制文件和实用程序。
在运行容器时不需要任何这些。你唯一需要的依赖就是Node.js。
Docker容器应该封装一个进程,并包含运行该进程所需的最少资源。你不需要操作系统。
事实上,除了Node.js,你可以删除所有东西。
怎么做到呢
幸运的是,谷歌也有同样的想法,并提出了GoogleCloudPlatform / distriless。
正如存储库的描述所指出的:
"Distroless" images contain only your application and its runtime dependencies. They do not contain package managers, shells any other programs you would expect to find in a standard Linux distribution.
这正是你所需要的!
你可以像这样调整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"]
build 一个新的docker镜像
$ docker build -t node-distroless .
我们来看看docker镜像的大小:
小了很多
但是你应该注意一些事情。
当你的容器正在运行时,如果你希望检查它,你可以给一个正在运行的容器命令:
docker exec -ti <insert_docker_id> bash
连接到一个正在运行的容器并运行bash去建立一个ssh会话。
但是由于distrless是原始操作系统的精简版本,所以没有额外的二进制文件。容器里没有shell
如果没有shell,你怎么能连接到一个正在运行的容器呢?
好消息和坏消息是,你不能。
这是一个坏消息,因为您只能执行容器中的二进制文件。你唯一能运行的二进制文件是Node.js:
docker exec -ti <insert_docker_id> node
这是一个好消息,因为攻击者利用您的应用程序并获得对容器的访问权限,不会像访问shell那样造成那么大的破坏。换句话说,更少的二进制文件意味着更小的大小和更高的安全性。但代价是更痛苦的调试。
Please note that perhaps you shouldn't attach to and debug containers in a production environment. You should rather rely on proper logging and monitoring
但是,如果您关心调试和更小的尺寸呢?
3. 用Alpine的更小的基础镜像
您可以用基于Alpine的镜像替换无失真基本镜像。 Alpine Linux 是:
a security-oriented, lightweight Linux distribution based on musl libc and busybox
换句话说,一个更小、更安全的Linux发行版。
你不应该把他们的话当作理所当然。让我们检查一下图像是否更小。
你应该调整Dockerfile并使用node:8-alpine:
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 .
看看他的大小:
让我们来启动容器并连接:
好像不行,但可以用sh去试一下
docker exec -ti 9d8e97e307d7 sh
是的! 您仍然可以连接到一个正在运行的容器,并且您拥有一个整体较小的镜像。
这听起来很有希望,但有一个问题。
基于Alpine的图像基于muslc——C语言的另一种标准库。
然而,大多数Linux发行版(如Ubuntu、Debian和CentOS)都是基于glibc的。这两个库应该实现到内核的相同接口。
然而,他们有不同的目标:
Glibc是最常见和最快的
Muslc使用更少的空间,并且在编写时考虑了安全性
在编译应用程序时,大多数情况下是针对特定的libc进行编译的。如果您希望在另一个libc中使用它们,则必须重新编译它们。
换句话说,使用Alpine映像构建容器可能会导致意外行为,因为标准C库是不同的。
在处理预编译的二进制文件(如Node.js c++扩展)时,您可能会注意到差异。
例如,PhantomJS预构建包不能在Alpine上工作。
那么我应该选择什么基础镜像?
你是用Alpine、原始镜像还是distroless的镜像?
如果您在生产环境中运行,并且担心安全性,那么可能distroless的镜像更合适。
添加到Docker镜像中的每个二进制文件都会给整个应用程序增加一定的风险。
您可以通过在容器中只安装一个二进制文件来降低总体风险。
例如,如果攻击者能够利用在Distroless上运行的应用程序中的漏洞,他们将无法在容器中生成shell,因为没有shell !
Please note that minimising attack surface area is recommended by OWASP.
如果您不惜一切代价关心大小,那么您应该切换到基于Alpine镜像。
它们通常非常小,但以兼容性为代价。Alpine使用一个稍有不同的标准C库——muslc。您可能会不时遇到一些兼容性问题。这里有更多的例子“alpine-node docker image and google-cloud = error loading ld-linux-x86-64.so”。这里的“导入错误试图在alpine上运行gRPC”。
原始基础镜像非常适合测试和开发。
它很大,但提供了与安装了Ubuntu的工作站相同的体验。此外,您还可以访问操作系统中可用的所有二进制文件。
图像大小概述: