
在构建Docker容器时,我们应尽可能减小镜像的大小。使用共享层的镜像尺寸越小,其传输和部署速度越快。不过在每个RUN语句都会创建一个新层的情况下,如果我们需要获取镜像完成前的中间产物,又如何控制其大小呢?你可能已经注意到市面上多数的Dockerfile都会使用类似这样的招数:FROM ubuntu
RUN apt-get update && apt-get install vim
FROM ubuntu
RUN apt-get update
RUN apt-get install vim
从Docker 1.10起,COPY、ADD和RUN语句会在镜像中添加新层。上述示例将创建两个层,而不是一个。Docker层存储了镜像上一版本和当前版本之间的差异。与Git提交类似,层有利于与其他仓库或镜像进行共享。实际上,当我们从Registry请求镜像时,我们只会下载那些不存在的层。这种方式让镜像共享更高效。层会占用空间,层越多,最终的镜像就越大。Git仓库在这方面是相似的。因为Git需要保存提交之间的所有变更,仓库的大小会随着层数的增加而增加。在过去,做法就是像第一个例子那样将几个RUN语句合并在一行中。
当Git仓库变得越来越大时,我们可以放弃所有过往信息,将历史提交合并成一个。使用Docker的多阶段构建,我们也能实现类似的功能。接下来的例子中,我们将构建一个Node.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!`)
})
{
"name": "hello-world",
"version": "1.0.0",
"main"
: "index.js",
"dependencies": {
"express": "^4.16.2"
},
"scripts": {
"start": "node index.js"
}
}
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
bc8c3cc813ae /bin/sh -c npm install 2.91MB
bac31afb6f42 /bin/sh -c
500a9fbef90e /bin/sh -c
78b28027dfbf /bin/sh -c
b87c2ad8344d /bin/sh -c
<missing> /bin/sh -c set -ex && for key in 6A010… 4.17MB
<missing> /bin/sh -c
<missing> /bin/sh -c ARCH= && dpkgArch="$(dpkg --print… 56.9MB
/bin/sh -c #(nop) ENV NODE_VERSION=8.9.4 0B
/bin/sh -c set -ex && for key in 94AE3… 129kB
/bin/sh -c groupadd --gid 1000 node && use… 335kB
/bin/sh -c set -ex; apt-get update; apt-ge… 324MB
/bin/sh -c apt-get update && apt-get install… 123MB
/bin/sh -c set -ex; if ! command -v gpg > /… 0B
/bin/sh -c apt-get update && apt-get install… 44.6MB
/bin/sh -c #(nop) CMD ["bash"] 0B
/bin/sh -c #(nop) ADD file:1dd78a123212328bd… 123MB
实际上,最终的镜像上添加了五个新层:Dockerfile里的每条语句一层。我们使用的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.js,还含有yarn、npm、bash以及大量其他二进制文件。同时,它是基于Ubuntu的。因此拥有一个完整的操作系统以及所有的二进制文件和实用程序。这些在运行容器时都不是必需的。我们唯一的依赖项是Node.js。Docker容器应封装在单一进程中,且只包含运行所需的最精简内容。我们不需要一个操作系统。幸运的是,Google也有同样的想法,他们带来了GoogleCloudPlatform/distroless[1]。“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
真是个好消息!不过在使用Distroless时有些事项需要注意。$ docker exec -ti bash
上述命令将附加到容器中并运行bash,这与发起一个SSH会话相近。不过由于Distroless是原始操作系统的精简版本,不包含额外的程序。容器里并没有Shell!坏消息是我们只能运行容器中的二进制程序。这里能运行的只有Node.js:
$ docker exec -ti node
好消息是因为没有Shell,如果黑客入侵了我们的应用程序并获取了容器的访问权限,他也无法造成太大的损害。也就是说,程序越少则尺寸越小也越安全。不过,代价是调试更麻烦。需要注意的是,我们不应该在生产环境中附加到容器中进行调试,而应依靠正确的日志和监控。如果我们既希望能调试,又关心尺寸大小,又该怎么办?我们可以使用Alpine取代Distroless来作为基础镜像。一个基于musl libc[3]和busybox[4]、面向安全的轻量级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
$ 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
/
很好!我们既可以附加到运行的容器中,得到的镜像尺寸也很小。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,因为根本就没有。如果更在意要是大小,则可以换成Alpine基础镜像。这两个都很小,代价是兼容性。Alpine用了一个稍稍有点不一样的C标准库——muslc。时不时会碰到点兼容性的问题。比如这个[6]和这个[7]。它的尺寸比较大,不过用起来就像你主机上安装的Ubuntu一样。并且,你能访问该操作系统里有的所有二进制程序。gcr.io/distroless/nodejs 76.7MBhttps://github.com/GoogleCloudPlatform/distroless
https://alpinelinux.org
https://www.musl-libc.org
https://www.busybox.net
https://www.owasp.org/index.php/Minimize_attack_surface_area
https://github.com/grpc/grpc/issues/8528
https://github.com/grpc/grpc/issues/6126
原文链接:https://itnext.io/3-simple-tricks-for-smaller-docker-images-f0d2bda17d1e
Kubernetes应用实战培训将于2018年9月14日在上海开课,3天时间带你系统掌握Kubernetes。本次培训包括:容器特性、镜像、网络;Docker特性、架构、组件、概念、Runtime;Docker安全;Docker实践;Kubernetes架构、核心组件、基本功能;Kubernetes设计理念、架构设计、基本功能、常用对象、设计原则;Kubernetes的实践、运行时、网络、插件已经落地经验;微服务架构、DevOps等,点击下方图片查看详情。
