NodeJS 服务 Docker 镜像极致优化指北( 二 )


在我们的服务中,由于运行该服务的依赖是确定的,因此为了尽可能的缩减基础镜像的体积,我们选择 alpine 版本作为生产环境的基础镜像 。
分级构建这时候,我们遇到了新的问题 。由于 alpine 的基本工具库过于简陋,而像 webpack 这样的打包工具背后可能使用的插件库极多,构建项目时对环境的依赖较大 。并且这些工具库只有编译时需要用到,在运行时是可以去除的 。对于这种情况,我们可以利用 Docker 的分级构建的特性来解决这一问题 。
首先,我们可以在完整版镜像下进行依赖安装,并给该任务设立一个别名(此处为build) 。
# 安装完整依赖并构建产物FROM node:14 AS buildWORKDIR /appCOPY package*.json /app/RUN ["npm", "install"]COPY . /app/RUN npm run build之后我们可以启用另一个镜像任务来运行生产环境,生产的基础镜像就可以换成 alpine 版本了 。其中编译完成后的源码可以通过--from参数获取到处于build任务中的文件,移动到此任务内 。
FROM node:14-alpine AS releaseWORKDIR /releaseCOPY package*.json /RUN ["npm", "install", "--registry=http://r.tnpm.oa.com", "--production"]# 移入依赖与源码COPY public /release/publicCOPY --from=build /app/dist /release/dist# 启动服务EXPOSE 8000CMD ["node", "./dist/index.js"]Docker 镜像的生成规则是,生成镜像的结果仅以最后一个镜像任务为准 。因此前面的任务并不会占用最终镜像的体积,从而完美解决这一问题 。
当然,随着项目越来越复杂,在运行时仍可能会遇到工具库报错,如果曝出问题的工具库所需依赖不多,我们可以自行补充所需的依赖,这样的镜像体积仍然能保持较小的水平 。
其中最常见的问题就是对node-gypnode-sass库的引用 。由于这个库是用来将其他语言编写的模块转译为 node 模块,因此,我们需要手动增加g++ make python这三个依赖 。
# 安装生产环境依赖(为兼容 node-gyp 所需环境需要对 alpine 进行改造)FROM node:14-alpine AS dependenciesRUN apk add --no-cache python make g++COPY package*.json /RUN ["npm", "install", "--registry=http://r.tnpm.oa.com", "--production"]RUN apk del .gyp

详情可见:https://github.com/nodejs/docker-node/issues/282
合理规划 Docker Layer构建速度优化我们知道,Docker 使用 Layer 概念来创建与组织镜像,Dockerfile 的每条指令都会产生一个新的文件层,每层都包含执行命令前后的状态之间镜像的文件系统更改,文件层越多,镜像体积就越大 。而 Docker 使用缓存方式实现了构建速度的提升 。若 Dockerfile 中某层的语句及依赖未更改,则该层重建时可以直接复用本地缓存 。
如下所示,如果 log 中出现Using cache字样时,说明缓存生效了,该层将不会执行运算,直接拿原缓存作为该层的输出结果 。
Step 2/3 : npm install ---> Using cache ---> efvbf79sd1eb通过研究 Docker 缓存算法,发现在 Docker 构建过程中,如果某层无法应用缓存,则依赖此步的后续层都不能从缓存加载 。例如下面这个例子:
COPY . .RUN npm install此时如果我们更改了仓库的任意一个文件,此时因为npm install层的上层依赖变更了,哪怕依赖没有进行任何变动,缓存也不会被复用 。
因此,若想尽可能的利用上npm install层缓存,我们可以把 Dockerfile 改成这样:
COPY package*.json .RUN npm installCOPY src .这样在仅变更源码时,node_modules的依赖缓存仍然能被利用上了 。
由此,我们得到了优化原则:

经验总结扩展阅读