如果你也因为不确定docker容器的安全性而惴惴不安;如果你也经常因为构建docker镜像而花费大量时间下载依赖,构建出特别大的镜像,那真的推荐你把这篇文章看完。
安全扫描
当你构建完一个镜像后,一个好的习惯是:使用dockerscan命令扫描安全漏洞。Docker与Synk合作,提供漏洞扫描服务。
比如说,要扫描之前在教程中创建的getting-started镜像,你可以输入:
dockerscangetting-started
扫描使用了不断更新的漏洞数据库,所以你看到提示应该和我这边的不太一样,不过大致是这样的:
输出的列表会列出漏洞的类型,详细信息的网址,以及最重要的:哪个版本修复了漏洞。
你也可以从dockerscan文档中了解命令的其它选项。
在命令行上扫描新构建的镜像的同时,你也可以配置DockerHub来自动扫描新推送的镜像,随后在DockerHub和DockerDesktop中看到扫描结果。
镜像分层
你知道么?你还可以查看是什么构成了一个镜像。使用dockerimagehistory命令,你能看到是什么命令创建了镜像内部的哪个分层。
1、使用dockerimagehistory命令查看getting-started镜像中的分层,对,就是之前教程中创建的那个。
dockerimagehistorygetting-started
你会看到类似下面的输出内容(日期或ID都可能会不一样):
每一行都代表了镜像中的一个分层。这里展示的内容,基础的分层在下面,最新的分层在上面。通过这个命令,你也可以快速查看每个分层的大小,帮你诊断超大镜像的问题。
2、此外,你还会发现有些行被截断了。如果想要获取完整输出不截断的话,需要加上--no-trunc旗标。
dockerimagehistory--no-truncgetting-started
不过在Windows的Powershell上输出的内容有些糟糕,不易阅读。为此我也在dockercli的仓库提了issue,等待官方的回复吧。
分层缓存
既然你已经见识过分层了,那就轮到重要的一课了。教你如何减少容器镜像的构建时间。
一旦某个分层改变后,所有的下游分层都必须重建。
让我们再看一眼我们之前使用的Dockerfile
FROMnode:12-alpine
WORKDIR/app
COPY..
RUNyarninstall--production
CMD["node","src/index.js"]
回到镜像历史的输出,我们看到Dockerfile中的每个命令都会转变为镜像中的分层。你可能还记得,当我们对镜像做修改的时候,yarn依赖必须重装一遍。有什么办法可以解决这个问题么?每次构建都要发布一遍相同的依赖有点说不过去了,对吧?
要解决这个问题,我们需要重新构建我们的Dockerfile,来实现依赖缓存。对于Node应用来说,依赖都是定义在package.json文件中的。那么,如果我们先只把那个文件拷贝过来,安装依赖,再拷贝剩余的其它文件呢?这样的话,只有package.json变更的情况下,才会重建yarn依赖。这样不是更合理么?
1、更新Dockerfile,先把package.json文件拷贝进来,安装依赖,然后再把其它文件拷贝进来。
2、创建.dockerignore文件,和Dockerfile放在同一个路径下,内容如下:
node_modules
.dockerignore文件帮我们选择性地拷贝仅与镜像相关的文件。在Dockerfilereference中能够看到更多相关信息。文件创建以后,node_moduels文件夹就会被第2个COPY步骤忽略了。不然的话,它很可能会被拷贝进容器,覆盖RUN步骤的命令所创建的依赖。关于Node.js应用为什么建议这么做,以及其它最佳实践,参考《将一个Node.js网页应用容器化》一文。
3、使用dockerbuild构建一个新镜像。
dockerbuild-tgetting-started.
能够看到类似这样的输出:
所有的层都被重建了。没毛病,因为我们把Dockerfile给改过了。注意,这次我们的RUN步骤花了39.6秒。
4、现在,对src/static/index.html文件做个小改动,比如把title改成"牛到不行的待办任务APP"。
5、再次使用dockerbuild-tgetting-started.构建镜像。这一次,输出的内容会有些不同:
RUN步骤只用了0.0秒。我们可以看到缓存了的步骤,前面都会加CACHED前缀。构建速度大大提升。同时,推拉镜像以及更新镜像都会快得多。
多阶段构建
这篇教程中,不会深入讨论关于多阶段构建的内容,不过多阶段构建确实是一个强大的工具。优点包括:
1、把构建时依赖和运行时依赖区分开
2、减少镜像大小,只发布应用运行所需的东西
以Maven/Tomcat为例
构建Java应用的时候,需要JDK来把源代码编译成Java字节码。不过,生产环境中,并不需要JDK。另外,你可能会使用类似Maven或Gradle来帮忙构建应用。这些工具在最终镜像中,其实也用不着。这个时候,多阶段构建就能起到作用了。
FROMmavenASbuild
WORKDIR/app
COPY..
RUNmvnpackage
FROMtomcat
COPY--from=build/app/target/file.war/usr/local/tomcat/webapps
这个例子中,我们使用了一个阶段,叫作build,这个阶段主要是使用Maven来执行真正的Java构建。第2个阶段从FROMtomcat开始,把文件从build阶段拷贝过来。最终的镜像只会创建最后一个阶段,这个行为可以使用--target旗标覆盖。
以React为例
构建React应用的时候,我们需要一个Node环境,以便把JS代码(通常是JSX),SASS样式表等等编译成静态的HTML,JS和CSS。如果我们不是在做服务器端渲染的话,生产环境构建甚至不需要Node环境。为什么不把静态资源发布到一个静态nginx容器呢?
FROMnode:12ASbuild
WORKDIR/app
COPYpackage*yarn.lock./
RUNyarninstall
COPYpublic./public
COPYsrc./src
RUNyarnrunbuild
FROMnginx:alpine
COPY--from=build/app/build/usr/share/nginx/html
这里,我们使用的是node:12镜像来执行构建(同时最大化了分层缓存),然后把输出文件拷贝进了nginx容器。怎么样,还不错吧?
回顾
通过对镜像的结构有一个初步了解,我们可以更快地构建镜像,发布更少的变更。扫描镜像给我们吃了定心丸,就目前而言,容器的运行和分发是安全的。多阶段构建帮助我们减少了镜像的容量。通过把构建时依赖和运行时依赖分离开,增加了最终镜像的安全性。
然后呢?
尽管我们的教程结束了,但是仍旧有很多关于容器的东西需要学习!我们这里不会展开太多,不过接下来还是有一些其它领域要去拓展的。
容器编排
生产环境中运行容器是艰难的。你不想登录到每台机器中去,仅仅是为了运行dockerrun或docker-