Docker Image 体积越大,那部署要花的时间就越长;假如每个版本都有好几 GB 也会拖慢服务打包编译的速度;因此笔者开始动手实践,想看看到底能将 Docker Image 的体积缩小多少!
ㄧ、先初始化一个简单的 Node.js 项目
bash
# 创建文件夹
mkdir docker-test
cd docker-test
# 初始化应用
npm init
# 安装 express
npm install express --save
初始化后的 package.json 大概会长这样(scripts 的 start 笔者有微调):
json
{
"name": "docker-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.1"
}
}
接著我们新增一个 index.js 的文件,文件内容如下:
javascript
const express = require('express')
const app = express()
const port = 3000
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
为了实测不同情况下打包的影响,我们使用 npm 安装ESlint 工具,并将其保存为开发依赖项:
npm install eslint -save-dev
二、编写 Dockefile,了解优化前体积有多大
这边我们就先用最简单的方式写 Dockerfile:
bash
FROM node
# 工作目录
WORKDIR /usr/src/app
# 拷贝所需文件
COPY package.json index.js ./
# 安装依赖
RUN npm install
# 提供服务的接口
EXPOSE 3000
CMD ["npm", "start"]
接下来输入 docker build -t docker-test .
就可以创建 Docker Image(docker-test 是名称)。
然后输入 docker images
,确认刚刚创建的 Docker Image 是否存在;从下图我们可以看到 优化前的 Image 大小高达 1.01GB 。
三、使用 Node.js 的 Alpine 版本
Node.js Alpine 版本的 Image 体积会远小于完整的 Node.js Image,现在我们修改一下 Dockerfile:
bash
# 使用 Alpine 版本
FROM node:alpine
WORKDIR /usr/src/app
COPY package.json index.js ./
RUN npm install
EXPOSE 3000
CMD ["npm", "start"]
输入 docker build -t docker-test-alpine .
来创建 Image,完成后输入 docker images
看看 build 出来的 Image 体积是否改变。
从上图我们可以看到,Alpine 的版本让它的体积从 1.01GB 下降到了 189MB,整整少了 812MB!
四、正式环境下,不需要安装 devDependencies 的依赖
通常一个项目会安装一些开发环境下的依赖,但这些依赖只需要在开发环境中辅助使用,在正式环境下并没有安装的必要。
我们调整一下 Dockerfile 安装依赖的指令:
sql
FROM node:alpine
WORKDIR /usr/src/app
COPY package.json index.js ./
# 只安装正式环境的依赖
RUN npm install --production
EXPOSE 3000
CMD ["npm", "start"]
在上图我们可以看到体积又变小了,果然还有优化的空间(少了 12MB)。
五、如果我们只使用最基础的 Alpine,然后 Node.js 自己安装呢?
刚刚我们使用的是 Node.js 的 Alpine 版本,如果更极端一点,只使用最基础的 Alpine,然后自己手动安装 Node.js 会有什么样的结果呢?
sql
# 使用最基础的 Alpine
FROM alpine:latest
# 自己安装 Node.js & npm
RUN apk add --no-cache --update nodejs npm
WORKDIR /usr/src/app
COPY package.json index.js ./
RUN npm install --production
EXPOSE 3000
CMD ["npm", "start"]
没想到这个操作把整包 Image 的体积压的更低了(从 177MB 降到 64.7MB)。
六、采用多阶段构建
Docker Image 你可以理解为很多层互相叠加在一起,从Docker 1.10开始,COPY、ADD 和 RUN 语句会向镜像中添加新层;而在 Docker 的世界中可以允许有多个「FROM」,但只会取用最后一个「FROM」所创建的 Image。
Docker的层用于保存镜像的上一版本和当前版本之间的差异。就像Git的提交一样,如果你与其他存储库或镜像共享它们,就会很方便。但额外的层并不是没有代价的,层仍然会占用空间,你拥有的层越多,最终的镜像就越大。
在了解上面的概念后,我们就可以把「安装编译」的步骤放在第一个「FROM」里面执行,然后第二个 FROM 就只是单纯地把第一层的结果搬过去即可,那么 Dockerfile 实现会长这样:
sql
FROM alpine:latest AS builder
RUN apk add --no-cache --update nodejs npm
WORKDIR /usr/src/app
COPY package.json index.js ./
# 先完成安裝编译
RUN npm install --production
FROM alpine:latest
RUN apk add --no-cache --update nodejs npm
WORKDIR /usr/src/app
# 把编译好的全部移动过来
COPY --from=builder /usr/src/app .
EXPOSE 3000
CMD ["npm", "start"]
经过这么多的努力后,我们的 Docker Image 也顺利从 1.01GB 下降到 58.9MB, 减少了超过 94% 的体积 ,并且可以明显感受到 build Image 的速度上升不少。
七、使用 Distroless 让正式环境更加安全
尽管上面我们已经使用 Alpine 让 Docker Image 变得这么小,但在文章的最后我想再提供读者另一个选择, 那就是「Distroless」!
先让我们用它来 Build 一个 Docker Image。
css
FROM node AS builder
WORKDIR /usr/src/app
COPY package.json index.js ./
RUN npm install --production
# 改成用 Distroless
FROM gcr.io/distroless/nodejs
WORKDIR /usr/src/app
COPY --from=builder /usr/src/app .
EXPOSE 3000
CMD ["index.js"]
如果单纯从结果来看,它在体积上(162MB)并没有什么优势,但如果你尝试用 Shell 打开它,会发现 Shell 根本不存在!换而言之,它的安全性相对更高,可以考虑使用它来做正式环境的部署。
以上就是笔者优化 Docker Image 体积的步骤啦!如果文中有表达不清晰、错误的部分再烦请告知。