三种方法让你的docker镜像体积更小

388 阅读7分钟

在构建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, ADDRUN语句会给你的镜像添加一个新层。前面的示例创建了两个层,而不是一个。

层就像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语句。所以你应该期望看到至少比基础镜像多两层:

image.png

相反,生成的镜像有五个新层: 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 .

现在看看构建历史:

image.png

对比看看2次build的镜像的大小:

image.png

是的,最后一个镜像稍微小一点。

还不错!您减少了总体大小,即使这是一个已经精简的应用程序。

但镜像仍然很大!

你能做些什么让它更小吗?

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镜像的大小:

image.png

小了很多

但是你应该注意一些事情。

当你的容器正在运行时,如果你希望检查它,你可以给一个正在运行的容器命令: 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 .

看看他的大小: image.png

让我们来启动容器并连接:

image.png

好像不行,但可以用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的工作站相同的体验。此外,您还可以访问操作系统中可用的所有二进制文件。

图像大小概述:

image.png