使用构建缓存

说明

考虑一下你为 入门 应用创建的 Dockerfile。

FROM node:20-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "./src/index.js"]

当你运行 `docker build` 命令来创建一个新的镜像时,Docker 会执行你 Dockerfile 中的每个指令,为每个命令创建一个层,并按照指定的顺序执行。对于每个指令,Docker 会检查它是否可以重用之前构建中的指令。如果发现你之前已经执行过类似的指令,Docker 就无需重新执行它。相反,它将使用缓存的结果。这样,你的构建过程就会更快、更高效,从而节省宝贵的时间和资源。

有效地使用构建缓存可以通过重用先前构建的结果并跳过不必要的工作来实现更快的构建。为了最大限度地利用缓存并避免资源密集型和耗时的重新构建,了解缓存失效的工作方式非常重要。以下是一些可能导致缓存失效的情况示例

  • 对 `RUN` 指令的任何命令更改都会使该层失效。如果你的 Dockerfile 中的 `RUN` 命令有任何修改,Docker 会检测到更改并使构建缓存失效。

  • 使用 `COPY` 或 `ADD` 指令复制到镜像中的任何文件的更改。Docker 会密切关注项目目录中文件的任何更改。无论是内容更改还是权限等属性更改,Docker 都将这些修改视为使缓存失效的触发器。

  • 一旦一层失效,所有后续层也会失效。如果任何之前的层(包括基础镜像或中间层)由于更改而失效,Docker 会确保依赖它的后续层也失效。这使构建过程保持同步并防止出现不一致。

编写或编辑 Dockerfile 时,请注意不必要的缓存未命中,以确保构建尽可能快速有效地运行。

试一试

在本实践指南中,你将学习如何有效地对 Node.js 应用程序使用 Docker 构建缓存。

构建应用程序

  1. 下载并安装 Docker Desktop。

  2. 打开终端并 克隆此示例应用程序

    $ git clone https://github.com/dockersamples/todo-list-app
    
  3. 进入 `todo-list-app` 目录

    $ cd todo-list-app
    

    在这个目录中,你会找到一个名为 `Dockerfile` 的文件,其内容如下:

    FROM node:20-alpine
    WORKDIR /app
    COPY . .
    RUN yarn install --production
    EXPOSE 3000
    CMD ["node", "./src/index.js"]
  4. 执行以下命令来构建 Docker 镜像:

    $ docker build .
    

    这是构建过程的结果:

    [+] Building 20.0s (10/10) FINISHED
    

    第一行表明整个构建过程耗时 *20.0 秒*。第一次构建可能需要一些时间,因为它需要安装依赖项。

  5. 无需更改地重新构建。

    现在,重新运行 `docker build` 命令,无需更改源代码或 Dockerfile,如下所示:

    $ docker build .
    

    只要命令和上下文保持不变,初始构建之后的后续构建速度就会更快,这是由于缓存机制。Docker 会缓存构建过程中生成的中间层。当你无需更改 Dockerfile 或源代码即可重新构建镜像时,Docker 可以重用缓存的层,从而显著加快构建过程。

    [+] Building 1.0s (9/9) FINISHED                                                                            docker:desktop-linux
     => [internal] load build definition from Dockerfile                                                                        0.0s
     => => transferring dockerfile: 187B                                                                                        0.0s
     ...
     => [internal] load build context                                                                                           0.0s
     => => transferring context: 8.16kB                                                                                         0.0s
     => CACHED [2/4] WORKDIR /app                                                                                               0.0s
     => CACHED [3/4] COPY . .                                                                                                   0.0s
     => CACHED [4/4] RUN yarn install --production                                                                              0.0s
     => exporting to image                                                                                                      0.0s
     => => exporting layers                                                                                                     0.0s
     => => exporting manifest
    

    后续构建仅用了 1.0 秒就完成了,这得益于缓存层。无需重复安装依赖项等耗时的步骤。

    步骤描述耗时(第一次运行)耗时(第二次运行)
    1从 Dockerfile 加载构建定义0.0 秒0.0 秒
    2加载 docker.io/library/node:20-alpine 的元数据2.7 秒0.9 秒
    3加载 .dockerignore0.0 秒0.0 秒
    4加载构建上下文

    (上下文大小:4.60MB)

    0.1 秒0.0 秒
    5设置工作目录 (WORKDIR)0.1 秒0.0 秒
    6将本地代码复制到容器中0.0 秒0.0 秒
    7运行 yarn install --production10.0 秒0.0 秒
    8导出层2.2 秒0.0 秒
    9导出最终镜像3.0 秒0.0 秒

    回到 `docker image history` 输出,你会看到 Dockerfile 中的每个命令都会成为镜像中的一个新层。你可能还记得,当你更改镜像时,必须重新安装 `yarn` 依赖项。有没有办法解决这个问题?每次构建都重新安装相同的依赖项没有多大意义,对吧?

    要解决这个问题,请重新构建你的 Dockerfile,以便依赖项缓存保持有效,除非确实需要使其失效。对于基于 Node 的应用程序,依赖项在 `package.json` 文件中定义。如果该文件发生更改,则需要重新安装依赖项,但如果文件未更改,则使用缓存的依赖项。因此,首先只复制该文件,然后安装依赖项,最后复制所有其他内容。然后,只有在 `package.json` 文件发生更改时,才需要重新创建 yarn 依赖项。

  6. 更新 Dockerfile,首先复制 `package.json` 文件,安装依赖项,然后复制所有其他内容。

    FROM node:20-alpine
    WORKDIR /app
    COPY package.json yarn.lock ./
    RUN yarn install --production 
    COPY . . 
    EXPOSE 3000
    CMD ["node", "src/index.js"]
  7. 在与 Dockerfile 相同的文件夹中创建一个名为 `.dockerignore` 的文件,内容如下:

    node_modules
  8. 构建新的镜像

    $ docker build .
    

    然后,你将看到类似于以下的输出:

    [+] Building 16.1s (10/10) FINISHED
    => [internal] load build definition from Dockerfile                                               0.0s
    => => transferring dockerfile: 175B                                                               0.0s
    => [internal] load .dockerignore                                                                  0.0s
    => => transferring context: 2B                                                                    0.0s
    => [internal] load metadata for docker.io/library/node:21-alpine                                  0.0s
    => [internal] load build context                                                                  0.8s
    => => transferring context: 53.37MB                                                               0.8s
    => [1/5] FROM docker.io/library/node:21-alpine                                                    0.0s
    => CACHED [2/5] WORKDIR /app                                                                      0.0s
    => [3/5] COPY package.json yarn.lock ./                                                           0.2s
    => [4/5] RUN yarn install --production                                                           14.0s
    => [5/5] COPY . .                                                                                 0.5s
    => exporting to image                                                                             0.6s
    => => exporting layers                                                                            0.6s
    => => writing image     
    sha256:d6f819013566c54c50124ed94d5e66c452325327217f4f04399b45f94e37d25        0.0s
    => => naming to docker.io/library/node-app:2.0                                                 0.0s
    

    你会看到所有层都被重新构建了。因为你对 Dockerfile 做了相当大的改动,所以这是完全正常的。

  9. 现在,更改 `src/static/index.html` 文件(例如,将标题更改为“The Awesome Todo App”)。

  10. 构建 Docker 镜像。这一次,你的输出应该看起来有点不同。

    $ docker build -t node-app:3.0 .
    

    然后,你将看到类似于以下的输出:

    [+] Building 1.2s (10/10) FINISHED 
    => [internal] load build definition from Dockerfile                                               0.0s
    => => transferring dockerfile: 37B                                                                0.0s
    => [internal] load .dockerignore                                                                  0.0s
    => => transferring context: 2B                                                                    0.0s
    => [internal] load metadata for docker.io/library/node:21-alpine                                  0.0s 
    => [internal] load build context                                                                  0.2s
    => => transferring context: 450.43kB                                                              0.2s
    => [1/5] FROM docker.io/library/node:21-alpine                                                    0.0s
    => CACHED [2/5] WORKDIR /app                                                                      0.0s
    => CACHED [3/5] COPY package.json yarn.lock ./                                                    0.0s
    => CACHED [4/5] RUN yarn install --production                                                     0.0s
    => [5/5] COPY . .                                                                                 0.5s 
    => exporting to image                                                                             0.3s
    => => exporting layers                                                                            0.3s
    => => writing image     
    sha256:91790c87bcb096a83c2bd4eb512bc8b134c757cda0bdee4038187f98148e2eda       0.0s
    => => naming to docker.io/library/node-app:3.0                                                 0.0s
    

    首先,你应该注意到构建速度快得多。你会看到几个步骤正在使用先前缓存的层。这是个好消息;你正在使用构建缓存。推送和拉取此镜像及其更新的速度也会快得多。

通过遵循这些优化技术,你可以使 Docker 构建更快、更高效,从而缩短迭代周期并提高开发效率。

附加资源

后续步骤

既然你已经了解了如何有效地使用 Docker 构建缓存,那么你就可以学习多阶段构建了。