让docker构建nodejs应用时使用npm缓存加速安装

我的node应用在原本的部署脚本下,每次部署都要十几分钟,而我的项目又不适合多阶段构建,于是想了各种办法让它使用镜像layer缓存一部分安装过程,但是多多少少都有点问题。

看来想要不出问题的话还是要在部署的时候让安装过程完整跑一次,这样要缩短部署时间就只能尽量利用npm缓存。

第一步:挂个外部缓存

我本来以为dockerfile构建过程中是没法像容器那样绑定一个volume来让过程文件持久化的,但是昨天我才发现其实是有的,就是要更新一下docker,这是Buildkit特性的一部分,这个特性在18.09版本之后的docker才有,总之尽量把docker升级到最新版就可以用了。

使用方法很简单,如下Dockerfile:

FROM node:18.18.1-buster-slim
RUN apt update &&\
	apt install -y git openssh-client python3 curl
COPY ./deploy /deploy
COPY ./app/ /app
RUN --mount=type=cache,target=/root/.npm \   #在RUN命令的开头这样写来挂一个缓存目录,这个mount参数需要在每个要使用此缓存的命令里都写一遍
	sh /deploy/install.sh   #然后执行你的安装脚本
WORKDIR "/deploy"
ENTRYPOINT ["/bin/sh"]
CMD [ "./start.sh" ]
EXPOSE 80

这样在这个dockerfile构建过程中就会把构建容器的`/root/.npm`映射到通用的docker缓存里,而且我测试下来如果在其它镜像里也挂载同样的缓存,那么其它镜像构建的时候也可以使用该缓存,但我其它镜像的FROM镜像都是相同的,不知道如果来源镜像变了是否会影响缓存挂载。

注意:~/.npm 是linux下npm默认的缓存目录,但默认缓存目录是可以更改的,如果你的项目里修改了npm的默认缓存目录地址,那么这里也要一起改。或者如果你的平台比较特殊,npm的默认目录本来就不在这里,那么也要进行对应的修改。

除了挂载缓存(type=cache)以外,还可以绑定外部文件或目录(type=bind),相关资料在这里https://docs.docker.com/build/guide/mounts/,不过这个文档有点迷惑,没写source和target哪个是里面哪个是外面的,我在source里面写外部路径它给我报文件不存在,然后我就直接用cache了,没再继续尝试。

如果你觉得构建过程缓存的文件有问题,或者单纯想清除这些缓存,可以根据这里的方法,执行以下命令:

docker builder prune --filter type=exec.cachemount

如果没有效果,可以尝试去掉–filter及其参数。

第二步:让npm优先使用缓存(可选)

npm在安装依赖时,即使本地有缓存,也会向服务器发起请求检查每个本地的缓存有没有过期,这个过程也很漫长。其实一般即使缓存过期了也问题不大,因为正常来说同一个版本号的包其内容是不会变的,所以可以让npm优先使用本地缓存,跳过检查其在线状态,这样可以大幅减少安装时间。

方法很简单,只要给安装命令加个`–prefer-offline`参数:

npm i --prefer-offline
#也可以再加个 --verbose 参数确认是否真的使用了缓存
npm i --prefer-offline --verbose

这样折腾完了之后,应用的后续部署时间在依赖没有改变的情况下从原来的十几分钟缩短到了一分多钟,堪称火箭级加速,总算解决了一个困扰我一年多的问题。

[docker]GDBus.Error:org.freedesktop.DBus.Error.ServiceUnknown: The name org.freedesktop.secrets was not provided by any .service files

这问题很奇怪,我在这部署了几个镜像都好好的,到其中一个的时候突然就报出了这样的错误:

failed to solve: node:18.18.1-buster-slim: error getting credentials - err: exit status 1, out: `GDBus.Error:org.freedesktop.DBus.Error.ServiceUnknown: The name org.freedesktop.secrets was not provided by any .service files`

然后查了下说跑这个命令安装依赖`apt install gnome-keyring`就好了,我试了一下确实解决了问题,但为什么好好的突然就不行了依然是个谜。

[docker]ERROR: Service ‘***’ failed to build: the –mount option requires BuildKit.

我在dockerfile中用了个–mount参数,结果一开始一直报`ERROR: Dockerfile parse error line 8: Unknown flag: mount`,然后我发现是我的docker版本太低了,于是升级了docker之后发现又变了个错误:

[docker]ERROR: Service '***' failed to build: the --mount option requires BuildKit. Refer to https://docs.docker.com/go/buildkit/ to learn how to build images with BuildKit enabled

我明明已经设置了两个环境变量

export DOCKER_BUILDKIT=1
export COMPOSE_DOCKER_CLI_BUILD=1

又研究了一会儿我发现原因出在我用的程序上,我执行的是docker-compose,这个版本还是1.17.1,但docker还内置了一个compose,它的版本是2.18.1,于是直接把命令换成docker compose就好了。