NodeJS 服务 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的依赖缓存仍然能被利用上了 。
由此,我们得到了优化原则:
  1. 最小化处理变更文件,仅变更下一步所需的文件,以尽可能减少构建过程中的缓存失效 。
  2. 对于处理文件变更的 ADD 命令、COPY 命令 , 尽量延迟执行 。
构建体积优化【NodeJS 服务 Docker 镜像极致优化指北】在保证速度的前提下 , 体积优化也是我们需要去考虑的 。这里我们需要考虑的有三点:
  1. Docker 是以层为单位上传镜像仓库的,这样也能最大化的利用缓存的能力 。因此,执行结果很少变化的命令需要抽出来单独成层,如上面提到的npm install的例子里 , 也用到了这方面的思想 。
  2. 如果镜像层数越少,总上传体积就越小 。因此,在命令处于执行链尾部,即不会对其他层缓存产生影响的情况下,尽量合并命令,从而减少缓存体积 。例如,设置环境变量和清理无用文件的指令 , 它们的输出都是不会被使用的,因此可以将这些命令合并为一行 RUN 命令 。
RUN set ENV=prod && rm -rf ./trash
  1. Docker cache 的下载也是通过层缓存的方式,因此为了减少镜像的传输下载时间,我们最好使用固定的物理机器来进行构建 。例如在流水线中指定专用宿主机 , 能是的镜像的准备时间大大减少 。
当然,时间和空间的优化从来就没有两全其美的办法,这一点需要我们在设计 Dockerfile 时,对 Docker Layer 层数做出权衡 。例如为了时间优化,需要我们拆分文件的复制等操作,而这一点会导致层数增多,略微增加空间 。

推荐阅读