一、前言

在构建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的提交(commit)一样。Docker的层用于保存镜像的上一版本和当前版本之间的差异。就像Git的提交一样,如果你与其他存储库或镜像共享它们,就会很方便。实际上,当你向注册表请求镜像时,只是下载你尚未拥有的层。这是一种非常高效地共享镜像的方式。但额外的层并不是没有代价的。层仍然会占用空间,你拥有的层越多,最终的镜像就越大。Git存储库在这方面也是类似的,存储库的大小随着层数的增加而增加,因为Git必须保存提交之间的所有变更。过去,将多个RUN语句组合在一行命令中或许是一种很好的做法,就像上面的第一个例子那样,但在现在看来,这样做并不妥。

一、 通过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://localhost: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里的一个语句。现在,让我们来试试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

最后一个镜像(node-multi-stage)更小一些。你已经将镜像的体积减小了,即使它已经是一个很小的应用程序。但整个镜像仍然很大!有什么办法可以让它变得更小吗?

二、 用distroless去除容器中所有不必要的东西

这个镜像包含了Node.js以及yarn、npm、bash和其他的二进制文件。因为它也是基于Ubuntu的,所以你等于拥有了一个完整的操作系统,其中包括所有的小型二进制文件和实用程序。但在运行容器时是不需要这些东西的,你需要的只是Node.js。Docker容器应该只包含一个进程以及用于运行这个进程所需的最少的文件,你不需要整个操作系统。实际上,你可以删除Node.js之外的所有内容。但要怎么做?所幸的是,谷歌为我们提供了distroless。以下是distroless存储库的描述:“distroless”镜像只包含应用程序及其运行时依赖项,不包含程序包管理器、shell以及在标准Linux发行版中可以找到的任何其他程序。这正是你所需要的!你可以对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.7MB!比之前的镜像小了600MB!但在使用distroless时有一些事项需要注意。当容器在运行时,如果你想要检查它,可以使用以下命令attach到正在运行的容器上:

$ docker exec -ti <insert_docker_id> bash

attach到正在运行的容器并运行bash命令就像是建立了一个SSH会话一样。但distroless版本是原始操作系统的精简版,没有了额外的二进制文件,所以容器里没有shell!在没有shell的情况下,如何attach到正在运行的容器呢?答案是,你做不到。这既是个坏消息,也是个好消息。之所以说是坏消息,因为你只能在容器中执行二进制文件。你可以运行的唯一的二进制文件是Node.js:

$ docker exec -ti <insert_docker_id> node

说它是个好消息,是因为如果攻击者利用你的应用程序获得对容器的访问权限将无法像访问shell那样造成太多破坏。换句话说,更少的二进制文件意味着更小的体积和更高的安全性,不过这是以痛苦的调试为代价的。或许你不应在生产环境中attach和调试容器,而应该使用日志和监控。但如果你确实需要调试,又想保持小体积该怎么办?

三、 小体积的Alpine基础镜像

你可以使用Alpine基础镜像替换distroless基础镜像。Alpine Linux是:一个基于musl libc和busybox的面向安全的轻量级Linux发行版。换句话说,它是一个体积更小也更安全的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 .

现在可以检查一下镜像大小:

$ docker images | grep node-alpine
node-alpine aa1f85f8e724 69.7MB
69.7MB!

甚至比distrless镜像还小!现在可以attach到正在运行的容器吗?让我们来试试。让我们先启动容器:

$ docker run -p 3000:3000 -ti --rm --init node-alpine
Example app listening on port 3000!

你可以使用以下命令attach到运行中的容器:

$ 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

看来不行,但或许可以使用shell?

$ docker exec -ti 9d8e97e307d7 sh / #

成功了!现在可以attach到正在运行的容器中了。看起来很有希望,但还有一个问题。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!请注意,OWASP本身就建议尽量减少攻击表面。如果你只关心更小的镜像体积,那么可以考虑基于Alpine的镜像。它们的体积非常小,但代价是兼容性较差。Alpine使用了略微不同的标准C库——muslc。你可能会时不时地遇到一些兼容性问题。原始基础镜像非常适合用于测试和开发。它虽然体积很大,但提供了与Ubuntu工作站一样的体验。此外,你还可以访问操作系统的所有二进制文件。再回顾一下各个镜像的大小:

node:8 681MB
node:8结合多阶段构建 678MB
gcr.io/distroless/nodejs 76.7MB
node:8-alpine 69.7MB

Docker学习总结(41)——三个技巧,将Docker镜像体积减小90%相关推荐

  1. Docker学习记录(三):配置 Docker 加速器:使用阿里云镜像仓库

    上两篇文章介绍了Windows环境下通过Docker Toolbox使用Docker,由于国内的网络问题,从Docker的官方镜像仓库获取镜像往往很慢,甚至经常失败,这篇文章介绍如何通过阿里云镜像库获 ...

  2. Docker学习之路04:创建定制Nginx镜像

    Docker学习之路04:创建定制Nginx镜像 Docker学习路线传送门: Docker学习之路01:Docker的安装 Docker学习之路02:阿里云镜像加速器 Docker学习之路03:Do ...

  3. docker学习笔记(三)镜像

    参考:汤小洋老师的教学视频 docker学习笔记(一)[docker 介绍.卸载.安装.配置加速] docker学习笔记(二)docker常用命令 docker学习笔记(四)使用docker搭建环境 ...

  4. Docker学习与和应用(二)_使用Docker

    在前一篇文章 Docker学习与和应用(一)_初步认识中,我们初步介绍了Docker解决了什么问题,Docker容器化技术与传统的虚拟化方式的区别,以及简要介绍了Docker的几大核心概念:镜像.容器 ...

  5. Docker学习总结(47)——温故Docker常用命令行

    一.前 言 Docker镜像是由Dockerfile和一些必要的依赖项组成的,Docker容器是动态的Docker镜像.要使用Docker命令,首先需要知道您是在处理镜像还是容器.一旦你知道你所处理的 ...

  6. Docker学习(一)Ubuntu版本的Docker安装和使用

    2019独角兽企业重金招聘Python工程师标准>>> Docker安装: [apt-get install docker.io   如果不成功先运行 apt-get update] ...

  7. docker学习(一)ubuntu上安装docker

    准备 你需要64位的ubuntu操作系统 Cosmic 18.10.Bionic 18.04 (LTS).Xenial 16.04 (LTS)其中之一. docker支持的架构为:x86_64 (or ...

  8. Docker学习总结(18)——阿里超大规模Docker化之路

    12月6-7日,由阿里巴巴集团.阿里巴巴技术发展部.阿里云云栖社区联合主办,以"2016双11技术创新"为主题的阿里巴巴技术论坛上,阿里巴巴研究员林昊分享了阿里超大规模Docker ...

  9. 三个技巧,将Docker镜像体积减小90%

    在构建Docker容器时,应该尽量想办法获得体积更小的镜像,因为传输和部署体积较小的镜像速度更快. \\ 但RUN语句总是会创建一个新层,而且在生成镜像之前还需要使用很多中间文件,在这种情况下,该如何 ...

最新文章

  1. Ubuntu 系统 卸载OpenJDK 8
  2. 全球富豪大洗牌!马斯克登顶世界首富,黄铮国内第三超马云
  3. 吴渴楨 160809206
  4. 向STM32串口发送数据的标准函数
  5. netcore部署到docker 实现excel生成_Docker部署Redis集群----第七节(docker-redis-sentinel集群实现篇)...
  6. ArcGIS Engine开发:框架/结构+对象库
  7. 服务器定期监控数据_机房环境监控的实践【斯必得智慧机房 】
  8. 如何在ant脚本中获得svn版本号
  9. L1-047 装睡 (10 分)—团体程序设计天梯赛
  10. ps aux、ps -aux、ps -ef之间的区别
  11. GitHub上整理的一些工具[转载]
  12. IOS逆向(9)DebugServer + LLDB
  13. 论文阅读:Few-Shot Object Detection with Attention-RPN and Multi-Relation Detector
  14. Strange Country II 【ZOJ - 3332】【竞赛图求解哈密顿通路】
  15. Android一行代码去掉百度导航的底部工具箱菜单
  16. 3D资产大掌柜—Connecter
  17. html、css合并表格边框
  18. Elasticsearch _reindex Alias使用
  19. wincc实现手机APP远程监控
  20. abp过滤规则android,ABP的数据过滤器(Data Filters)

热门文章

  1. QT每日一练day13:QFileDialog文件浏览框
  2. C++STL笔记(三):array详解
  3. C++之文件操作探究(三):写文件——二进制文件
  4. oracle约束或语句,oracle、DB2、Informix常用约束语句
  5. 安全中心登录_放心使用instagram需要做到的5个信息安全设置
  6. win10锁定计算机会断网吗,Win10专业版如何设置锁屏后不断网?超详细的图文教程...
  7. mysql三高讲解(一):1.1 客户端怎样连接mysql数据库
  8. 数据结构专题(二):2.2单链表与顺序表,求元素个数,取值与查找
  9. 荣耀平板5升级鸿蒙,荣耀能否“升级”鸿蒙?赵明正式确认:华为做得好会考虑采用!...
  10. 服务器比虚拟主机安全,虚拟主机比较安全配置