再谈 Docker 如何缓存前端依赖

本文正在参加「金石计划」

再续 Docker 前缘

接上篇一个前端眼中的 Docker 一文,最后提到了 Docker 多阶段构建以及利用缓存的优化,但是却没有细说,多阶段缓存如何利用缓存,缓存的逻辑是怎样的,如果是在真实环境中,使用 CI 工具构建,又该如何利用缓存呢?本篇也是尝试解决这些问题,如果对 Docker 的基础概念还不了解,可以去看上文复习一下。

再说多阶段构建

多阶段构建就是把一个完整的构建流程拆分成几个小步骤,对于前端来说,构建的流程大多是这样的:先下载依赖,然后使用构建工具进行构建,最后将静态资源放到 Nginx 服务上。随便找个项目作为示例,根据以上流程,Dockerfile 文件的内容可以是这样的:

FROM node:16-buster-slim as dependency

COPY . /var/web

RUN set -x \
&& cd /var/web \
&& npm install

FROM node:16-buster-slim as builder

COPY --from=0 /var/web /var/web

COPY . /var/web

RUN set -x \
&& cd /var/web \
&& npm run build

FROM nginx:1.23.1 as prod

EXPOSE 8003

COPY --from=1 /var/web/nginx.conf /etc/nginx

COPY --from=1 /var/web/dist /usr/share/nginx/html

CMD [ "nginx", "-g", "daemon off;" ]

可以看到多阶段构建,就是多了几个FROM语句嘛,这么做对比原来分开多个 dockerfile 文件有什么好处呢?主要有以下三个方面:

  • 减小镜像体积

首先多阶段构建最终生成的镜像只包含了最后一个阶段的产物,自然比所有阶段加起来生成的镜像体积要小。dockerfile 中的每一条语句都是镜像中的一层,这些层最终联合起来就组成了镜像,所以在 dockerfile 中会看到这样的写法:

RUN cd /app \
&& npm run build

使用&&把两条命令合并成一条,最终生成的镜像也只有一层。

  • 减少镜像构建时间

其次的好处就是可以利用缓存。上文提到,Docker 会把所有的层联合起来,这里联合并不是简单的把层相加就可以了,每次构建都会生成缓存,下次构建如果层的信息没有发生变化,则可以复用该层。

当然,如果不想使用缓存,可以添加--no-cache的命令禁用缓存。

  • 更方便管理镜像

把多个构建步骤放入一个 dockerfile 文件中,不再需要多个项目中跳来跳去,如果依赖版本变更,不再需要先构建依赖镜像,再去更新项目的依赖版本,最后再去构建项目镜像这么繁琐的步骤。

多阶段构建的问题

多阶段构建在特定场景下,也会存在问题。

如果我们不是通过脚本或者是命令行本地运行构建镜像的命令,而是利用 CI 工具,比如 Github Action、Gitlab CI 这种。这样的话,每次构建的环境都是一个全新的环境,那就无法利用镜像缓存的优势,导致每次构建都在做重复的工作,降低了构建效率。

这种环境下又怎么利用缓存呢?

如何解决 CI 环境中的缓存失效

Docker 提供了--cache-from选项指定作为缓存的镜像,构建的时候就能利用上次构建的镜像作为缓存来加速项目镜像的构建,以 Github Action 为例,在项目中新建一个 yaml 的文件,内容如下:

name: Docker Image CI

on:
  push:
    branches: [ "final" ]
env:
  BaseImage: qiugu/dep
  MusicImage: qiugu/music

jobs:
  build:
    name: Log in and push Docker image
    runs-on: ubuntu-latest
    steps:
      - name: Log in to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWD }}

      - uses: actions/checkout@v3
      - name: Build the Docker image
        run: |
          docker pull $BaseImage || true
          docker pull $MusicImage || true
          docker build -t $BaseImage --target dependency --cache-from $BaseImage .
          docker build -t $MusicImage --cache-from $BaseImage .
          docker push $BaseImage
          docker push $MusicImage

配置文件中的步骤很简单,就两步,首先登录 DockerHub,其次执行脚本,起到作用的关键就是下面几行脚本:

# BaseImage 和 MusicImage 镜像名称都是上面 env 选项中配置的脚本变量
docker pull $BaseImage || true
docker pull $MusicImage || true
docker build -t $BaseImage --target dependency --cache-from $BaseImage .
docker build -t $MusicImage --cache-from $BaseImage .
docker push $BaseImage
docker push $MusicImage

解释一下上面的脚本:先拉取 DockerHub 中的基础镜像和项目镜像,下载到本地以后,进行基础镜像和项目镜像的构建,添加--cache-from选项指定上次构建的镜像作为缓存,接着将构建好的新的镜像推送到 DockerHub 上。

为什么要这么做呢?--cache-from选项是将上次构建的镜像作为缓存使用,因此需要先从仓库中拉取作为缓存的镜像,不然每次 CI 环境中是不存在这个缓存镜像的。拉取到上次构建的镜像以后,就可以构建本次的镜像了,带上--cache-from选项,如果本地已经存在该选项指定的镜像,则再次构建的时候,就能使用缓存了。不过,因为首次运行 CI 的时候,两个镜像都不存在,所以后面加上了|| true语句,保证脚本能够继续执行。

来看看第一遍 CI 的执行过程:

再谈 Docker 如何缓存前端依赖

Ok,没有问题,对于前端应用来说,构建的大部分耗时都在npm install这一步上。

再谈 Docker 如何缓存前端依赖

可以看到,install 这一步就花了一分多钟,我们作为示例的项目,node_modules 依赖包还是比较小的情况下,真实项目中 install 的时间会更长,所以主要的优化就是体现在把项目的 node_modules 包的依赖镜像作为缓存,而不是每次都需要下载依赖。

看看 Docker 中构建的镜像是否已经推送到 DockerHub 了:

再谈 Docker 如何缓存前端依赖

也没有问题,那么理论上,下次构建就可以使用缓存了。

随便在项目中修改一下内容,添加个空格之类的,用来触发 CI 构建就可以了,再来观察执行结果:

再谈 Docker 如何缓存前端依赖

尴尬了,这次显示还是重新执行的npm install命令,并且花费的时间和上次基本差不多:

再谈 Docker 如何缓存前端依赖

这是为什么,明明已经使用了--cache-from了啊?关键不在于--cache-from选项有问题,而是我们上面的 dockerfile 文件有问题:

COPY . /var/web 

这一句就是将当前文件夹下面的内容都拷贝到 /var/web 下面去,当前文件夹下面的内容就是上篇文章中提到的上下文,不了解的话可以翻过去重新看一下。当上下文内容改变时(我们为了提交,添加了一个空格),docker 会丢弃原有的缓存,重新构建镜像,并且其后的所有层,都会丢弃缓存。

那如果只复制 package.json 和 package-lock.json 文件,这两个文件依赖没有变动的情况下,可以使用缓存镜像,一旦我们的依赖需要更新,缓存失效,就可以构建新的基础镜像,正好解决了依赖更新时,项目镜像还是会使用老的缓存镜像的问题。

原因知道了,按照上面修改一下 dockerfile 文件:

COPY ./package.json ./package-lock.json /var/web

然后提交,查看运行结果:

再谈 Docker 如何缓存前端依赖

再谈 Docker 如何缓存前端依赖

第一张图是构建基础镜像,第二张图则是构建项目镜像,构建基础镜像时依然重新执行了npm install,这是因为我们修改了 dockerfile 文件的内容,那么镜像中的该层内容也会发生变化,缓存则会失效。构建好基础镜像以后,再构建项目镜像,已经有缓存镜像,所以项目镜像中 npm install 使用了缓存。

让我们再次添加一个空格,提交构建:

再谈 Docker 如何缓存前端依赖

两次构建都使用了缓存,因为此次构建,镜像中各层的内容都没有发生改变,所以就可以复用缓存中的内容。此时构建时间也是大大缩短:

再谈 Docker 如何缓存前端依赖

–cache-from 的问题

--cache-from也会存在问题。就是构建的时候,每次都会推送新的镜像到仓库中,即使基础镜像并没有改变。虽然基础镜像也使用了缓存,内容也没有任何变更,只是镜像仓库中的构建时间每次构建时都发生了变化。

这也是--cache-from的一个小的缺陷。

另外需要注意的是,Docker 的多阶段构建功能需要你的 Docker 的版本在 17.05 以上才可以使用。

完成目标

至此,我们在 CI 缓存中实现依赖镜像缓存的功能就实现了,不再需要分开构建多个镜像,多个配置文件中挑来跳去,简化了开发流程。

原文链接:https://juejin.cn/post/7215967453914234938 作者:qiugu

(0)
上一篇 2023年3月30日 下午5:02
下一篇 2023年3月30日 下午5:13

相关推荐

发表评论

登录后才能评论