在构建容器镜像时,我们总是希望得到尺寸更小的镜像。比如尽可能的减少镜像中的层数,因为创建新的层是有代价的,每个层都会产生一些数据上的开销。常见的手段是通过 && 把多个 RUN 指令合并为一个:
# 两个 RUN 指令会创建两个镜像层
FROM ubuntu
RUN apt-get update
RUN apt-get install vim
# 通过 && 把两个 RUN 指令合并为一个,
# 这样只会创建一个镜像层,
# 从而减小最终镜像的尺寸
FROM ubuntu
RUN apt-get update && apt-get install vim
而现在,我们有了更多的选择。我们可以使用 multi-stage 技术(关于 multi-stage 技术,请参考笔者的博文《Dockerfile 中的 multi-stage》)来减小容器镜像的尺寸,并且还可以使用 multi-stage 技术结合不同的父镜像来极大的减小最终的镜像的尺寸。有没有很心动呢?接下来就让我们通过 demo 演示笔者砍掉镜像虚膘的三板斧!
通过 multi-stage 减小镜像尺寸
让我们创建一个 host 在容器中的 nodejs 程序,并用它来进行本文的 demo 演示。先创建 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"]
我们使用父镜像 node:8 来构建并运行上面的 nodejs 应用:
$ docker build -t node-demo .
$ docker run -p 3000:3000 -ti --rm --init node-demo
这样应用程序就运行起来了,然后可以通过 http://localhost:3000/ 访问它。
现在让我们把注意力转移到运行容器的镜像 node-demo 身上,先看看它的构建历史:
$ docker history node-demo
我们的构建过程只是在父进行的基础上增了 3M 多一点的数据。
下面在 Dockerfile 中使用 multi-stage(关于 multi-stage 技术,请参考笔者的博文《Dockerfile 中的 multi-stage》):
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 ["npm", "start"]
把上面的内容保存到 Dockerfile.multi 文件中,然后构建镜像 node-demo-multi:
$ docker build --no-cache -t node-demo-multi . -f Dockerfile.multi
构建成功后看看 node-demo-multi 的历史:
$ docker history node-demo-multi
这次在父镜像的基础上增加的数据更少,总共只有 1.63M,应该是 multi-stage 在中间过程中进行了层合并的结果。然后再对比一下两个镜像的大小:
$ docker images|grep node-demo
在我们的场景下使用了 multi-stage 产生的镜像只比没有使用的情况小了 1M,但是对于镜像层数比较多的场景效果会更加明显。关于 multi-stage 的更多内容,请参考笔者的博文《Dockerfile 中的 multi-stage》。
移除镜像中的非必要内容
以我们使用的父镜像 node:8 为例:
其 676M 的体积中包含了众多运行 nodejs 应用并不需要的程序,比如 npm,bash 和其它众多的文件。如果我们能够把这些不需要的文件全部移除,镜像就会缩小很多。但是,具体该怎么做呢?答案是:使用合适的父镜像!
Google 在 github 上创建了 distroless 项目专门来做这件事!Distroless 项目提供的容器镜像只包含运行应用程序的最新集合,所以能够把镜像压缩到很小。但是需要为不同的应用程序使用不同的镜像,下面是当前已经提供的镜像:
让我们使用 distroless 提供的 nodejs 镜像来重新构建 demo 应用的镜像:
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"]
把上面的内容保存到文件 Dockerfile.less 中,并运行下面的命令:
$ docker build --no-cache -t node-demo-less . -f Dockerfile.less
$ docker run -p 3000:3000 -ti --rm --init node-demo-less
应用程序可以正常的运行,然后让我们看看刚才构建的容器镜像 node-demo-less:
只有 76.7M!这确实太不可思议了,在吃惊之于让我们回归理性,看看 distroless 究竟是如何把镜像做的这么小?我想先用 docker exec 命令进入容器内部看看情况,结果是我无法用下面的命令进入到容器内部:
$ docker exec -it <container id> bash
结论是为了减小镜像的大小,镜像中没有 bash 这样的工具。那么镜像中有什么?答案是只有 nodejs。你唯一能通过 docker exec 运行的命令就是:
$ docker exec -it <container id> node
这同样让人大吃一惊,不能进入容器的话可怎么调查故障?但现实就是这么残酷,当你享受极小的镜像时,你也为方便性付出了代价。其实故障调查完全可以通过完善的日志系统来解决。镜像中只有一个程序带来的另一个好处是安全性的提升!
使用 Alpine 作为 base 镜像
除了 Google 的 distroless 项目,其实我们还可以有其他的选择,它就是 Alpine。使用 Alpine 作为父镜像同样也会为你带来意想不到的惊喜(如果你留意了《Dockerfile 中的 multi-stage》一文中的镜像大小)!
Alpine Linux 是一个基于 musl libc 和 busybox 的,以安全性为目标的轻量级 Linux 发行版。换句话说就是:它的 size 更小,安全性更高!
接下来让我们用 Alpine 版的父镜像构建 demo 程序镜像:
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"]
把上面的内容保存到文件 Dockerfile.alpine 中,并运行下面的命令:
$ docker build --no-cache -t node-demo-alpine . -f Dockerfile.alpine
$ docker run -p 3000:3000 -ti --rm --init node-demo-alpine
应用程序可以正常的运行,然后让我们看看刚才构建的容器镜像 node-demo-alpine:
只有 69.7M,比使用 distroless 项目创建的镜像还要小!能用 docker exec 命令进入容器吗?让我们运行一个命名的容器试试:
$ docker run -p 3000:3000 -d --name democon --init node-demo-alpine
$ docker exec -it democon sh
这次我们成功的进入了容器,虽然不支持 bash,但能有个 shell 哥们就感觉幸福无边了!
Alpine Linux 看起来很完美,但是请把接下来的文章读完!
Alpine Linux 中的 C 库不是我们常用的 glibc,而是 muslc。也就是说,使用基于 Alpine 的镜像有可能产生 C 库不同导致的问题。举个例子,PhantomJS 就不能在 Alpine 中正常工作。
我们究竟该如何选择父镜像
- 如果是在生产环境中使用,并且有安全性的考虑,建议使用 distroless 镜像。
- 如果要保持尽可能小的镜像,建议使用 alpine 进行。
- 开发、测试环境中建议使用官方镜像通过 multi-stage 构建,方便问题调查。
下表展示了使用不同方式构建的镜像大小:
总结
控制镜像的大小是一件永远在路上的事情,使用 multi-stage 技术并选择合适的父镜像可以把我们从繁琐的操作中解放出来,这就是生产力啊!需要注意的是,你需要小心的选择父镜像,选择体积又小又适合你的应用程序的镜像可是个技术活儿呢!
参考:
3 simple tricks for smaller Docker images