在 Docker 卷中缓存 Cargo 依赖项

Cache Cargo dependencies in a Docker volume

我正在 Docker (rust:1.33.0) 中构建一个 Rust 程序。

每次代码更改时,它都会重新编译(好),这也会重新下载所有依赖项(坏)。

我想我可以通过添加 VOLUME ["/usr/local/cargo"] 来缓存依赖项。 编辑 我也试过用 CARGO_HOME 移动这个目录,但没有成功。

我认为将其设为一个卷会保留下载的依赖项,这些依赖项似乎位于此目录中。

但是没有用,每次都还在下载。为什么?


Docker文件

FROM rust:1.33.0

VOLUME ["/output", "/usr/local/cargo"]

RUN rustup default nightly-2019-01-29

COPY Cargo.toml .
COPY src/ ./src/

RUN ["cargo", "build", "-Z", "unstable-options", "--out-dir", "/output"]

仅用 docker build . 构建。

Cargo.toml

[package]
name = "mwe"
version = "0.1.0"
[dependencies]
log = { version = "0.4.6" }

代码:只是你好世界

更改main.rs后第二个运行的输出:

...
Step 4/6 : COPY Cargo.toml .
---> Using cache
---> 97f180cb6ce2
Step 5/6 : COPY src/ ./src/
---> 835be1ea0541
Step 6/6 : RUN ["cargo", "build", "-Z", "unstable-options", "--out-dir", "/output"]
---> Running in 551299a42907
Updating crates.io index
Downloading crates ...
Downloaded log v0.4.6
Downloaded cfg-if v0.1.6
Compiling cfg-if v0.1.6
Compiling log v0.4.6
Compiling mwe v0.1.0 (/)
Finished dev [unoptimized + debuginfo] target(s) in 17.43s
Removing intermediate container 551299a42907
---> e4626da13204
Successfully built e4626da13204

您不需要使用明确的 Docker 卷来缓存您的依赖项。 Docker 将自动缓存图像的不同 "layers"。基本上,Docker文件中的每个命令都对应图像的一层。您面临的问题基于 Docker 图像层缓存的工作方式。

Docker遵循的镜像层缓存规则在官方documentation:

中列出
  • Starting with a parent image that is already in the cache, the next instruction is compared against all child images derived from that base image to see if one of them was built using the exact same instruction. If not, the cache is invalidated.

  • In most cases, simply comparing the instruction in the Dockerfile with one of the child images is sufficient. However, certain instructions require more examination and explanation.

  • For the ADD and COPY instructions, the contents of the file(s) in the image are examined and a checksum is calculated for each file. The last-modified and last-accessed times of the file(s) are not considered in these checksums. During the cache lookup, the checksum is compared against the checksum in the existing images. If anything has changed in the file(s), such as the contents and metadata, then the cache is invalidated.

  • Aside from the ADD and COPY commands, cache checking does not look at the files in the container to determine a cache match. For example, when processing a RUN apt-get -y update command the files updated in the container are not examined to determine if a cache hit exists. In that case just the command string itself is used to find a match.

Once the cache is invalidated, all subsequent Dockerfile commands generate new images and the cache is not used.

所以问题出在命令 COPY src/ ./src/Dockerfile 中的定位。每当您的一个源文件发生变化时,缓存就会失效,所有后续命令都不会使用缓存。因此,您的 cargo build 命令将不会使用 Docker 缓存。

要解决您的问题,只需将 Docker 文件中的命令重新排序即可:

FROM rust:1.33.0

RUN rustup default nightly-2019-01-29

COPY Cargo.toml .

RUN ["cargo", "build", "-Z", "unstable-options", "--out-dir", "/output"]

COPY src/ ./src/

这样做,只有当您的 Cargo.toml.

发生变化时,才会重新安装您的依赖项

希望对您有所帮助。

以下是可能性的概述。 (向下滚动查看我的原始答案。)

  • 添加 Cargo 文件,创建假 main.rs/lib.rs,然后编译依赖项。然后删除假源并添加真实源。 [缓存依赖项,但有几个带有工作区的假文件]。
  • 添加 Cargo 文件,创建假 main.rs/lib.rs,然后编译依赖项。然后使用依赖项创建一个新层并从那里继续。 [同上].
  • 为缓存目录外部安装一个卷。 [缓存所有内容,依赖调用者传递 --mount]。
  • 在新 Docker 版本的 Docker 文件中使用 RUN --mount=type=cache,target=/the/path cargo build。 [缓存一切,似乎是个好方法,但目前对我来说太新了。可执行文件不是图像的一部分]
  • 运行 在另一个容器或主机上缓存,然后在构建过程中连接到它。请参阅货物问题 2644 中的 this comment
  • 使用cargo-build-deps。 [可能对某些人有用,但不支持 Cargo 工作区(2019 年)]。
  • 等待 Cargo issue 2644。 [有意愿将此添加到 Cargo,但还没有具体的解决方案]。
  • 在 Docker 文件中使用 VOLUME ["/the/path"] 不起作用 ,这仅适用于每层(每个命令)。

注意:可以在 Docker 文件中设置 CARGO_HOMEENV CARGO_TARGET_DIR 来控制下载缓存和编译输出的位置。

另请注意:cargo fetch 至少可以缓存依赖项的下载,尽管不能编译。

Cargo 工作空间必须手动添加每个 Cargo 文件,并且对于某些解决方案,必须生成一打假 main.rs/lib.rs。对于具有单个 Cargo 文件的项目,解决方案效果更好。


我已经通过添加

来为我的特定情况使用缓存
ENV CARGO_HOME /code/dockerout/cargo
ENV CARGO_TARGET_DIR /code/dockerout/target

其中 /code 是我挂载代码的目录。

这是外部安装的,而不是来自 Docker 文件。

EDIT1:我很困惑为什么这有效,但@b.enoit.be 和@BMitch 澄清说这是因为只在 Docker 文件中声明了卷活一层(一个命令)。

Dockerfile 中的卷在这里适得其反。这将在每个构建步骤安装一个匿名卷,并在您 运行 容器时再次安装。每个构建步骤中的卷在该步骤完成后被丢弃,这意味着您需要为需要这些依赖项的任何其他步骤再次下载全部内容。

此标准模型是复制您的依赖项规范,运行 依赖项下载,复制您的代码,然后编译或 运行 您的代码,分为 4 个单独的步骤。这让 docker 以高效的方式缓存图层。我对 rust 或 cargo 不熟悉,但我相信它看起来像:

FROM rust:1.33.0

RUN rustup default nightly-2019-01-29

COPY Cargo.toml .
RUN cargo fetch # this should download dependencies
COPY src/ ./src/

RUN ["cargo", "build", "-Z", "unstable-options", "--out-dir", "/output"]

另一种选择是使用 BuildKit (available in 18.09, released 2018-11-08) 打开一些实验性功能,以便 docker 将这些依赖项保存在类似于构建的命名卷中。该目录可以跨构建重复使用,但永远不会添加到图像本身,这使其可用于下载缓存等用途。

# syntax=docker/dockerfile:experimental
FROM rust:1.33.0

VOLUME ["/output", "/usr/local/cargo"]

RUN rustup default nightly-2019-01-29

COPY Cargo.toml .
COPY src/ ./src/

RUN --mount=type=cache,target=/root/.cargo \
    ["cargo", "build", "-Z", "unstable-options", "--out-dir", "/output"]

请注意,上面假设 cargo 在 /root/.cargo 中缓存文件。您需要对此进行验证并进行适当调整。我也没有将 mount 语法与 json exec 语法混合来了解该部分是否有效。您可以在此处阅读有关 BuildKit 实验性功能的更多信息:https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/experimental.md

从 18.09 和更新版本打开 BuildKit 就像 export DOCKER_BUILDKIT=1 然后 运行 从 shell.

构建你的版本一样简单

我会说,更好的解决方案是求助于 docker multi-stage build as pointed here and there

通过这种方式,您可以创建自己的第一个映像,它将构建您的应用程序和依赖项,然后仅在第二个映像中使用第一个映像中的依赖项文件夹

这受到您对 的评论和上面链接的两个问题评论的启发。

FROM rust:1.33.0 as dependencies

WORKDIR /usr/src/app

COPY Cargo.toml .

RUN rustup default nightly-2019-01-29 && \
    mkdir -p src && \
    echo "fn main() {}" > src/main.rs && \
    cargo build -Z unstable-options --out-dir /output

FROM rust:1.33.0 as application

# Those are the lines instructing this image to reuse the files 
# from the previous image that was aliased as "dependencies" 
COPY --from=dependencies /usr/src/app/Cargo.toml .
COPY --from=dependencies /usr/local/cargo /usr/local/cargo

COPY src/ src/

VOLUME /output

RUN rustup default nightly-2019-01-29  && \
    cargo build -Z unstable-options --out-dir /output

PS:只有一个运行会减少生成的层数;更多信息 here

我确定您可以调整此代码以与 Dockerfile 一起使用,但我 wrote a dockerized drop-in replacement for cargo 您可以将其保存到包中,运行 为 ./cargo build --release仅适用于(大多数)开发(使用rust:latest),但未针对CI或任何其他设置。

用法:./cargo build./cargo build --release、等等

它将使用当前工作目录并将缓存保存到./.cargo。 (您可以在版本控制中忽略整个目录,它不需要预先存在。)

在项目的文件夹中创建一个名为 cargo 的文件,运行 chmod +x ./cargo 放在上面,并在其中放置以下代码:

#!/bin/bash

# This is a drop-in replacement for `cargo`
# that runs in a Docker container as the current user
# on the latest Rust image
# and saves all generated files to `./cargo/` and `./target/`.
#
# Be sure to make this file executable: `chmod +x ./cargo`
#
# # Examples
#
# - Running app: `./cargo run`
# - Building app: `./cargo build`
# - Building release: `./cargo build --release`
#
# # Installing globally
#
# To run `cargo` from anywhere,
# save this file to `/usr/local/bin`.
# You'll then be able to use `cargo`
# as if you had installed Rust globally.
sudo docker run \
    --rm \
    --user "$(id -u)":"$(id -g)" \
    --mount type=bind,src="$PWD",dst=/usr/src/app \
    --workdir /usr/src/app \
    --env CARGO_HOME=/usr/src/app/.cargo \
    rust:latest \
    cargo "$@"

随着 BuildKit 集成到 docker,如果您能够利用高级 BuildKit 后端,现在可以 mount a cache volume during a RUN command,恕我直言,这已成为最好的方式缓存货物构建。缓存卷保留了之前运行时写入的数据。

要使用 BuildKit,您需要安装两个缓存卷,一个用于缓存外部 crate 源的 cargo 目录,另一个用于缓存所有构建的工件,包括外部 crate 和项目的目标目录垃圾箱和库。

如果您的基本图像是 rust,$CARGO_HOME 设置为 /usr/local/cargo,那么您的命令如下所示:

RUN --mount=type=cache,target=/usr/local/cargo,from=rust,source=/usr/local/cargo \
    --mount=type=cache,target=target \
    cargo build

如果您的基础图像是其他东西,您需要将 /usr/local/cargo 位更改为 $CARGO_HOME 的值,或者添加 ENV CARGO_HOME=/usr/local/cargo 行。作为旁注,聪明的做法是直接设置 target=$CARGO_HOME 并让 Docker 进行扩展,但它 似乎无法正常工作 - 发生了扩展,但是当您执行此操作时,buildkit 仍然不会在运行中保持相同的体积。

实现 Cargo 构建缓存的其他选项(包括 sccache 和 cargo wharf 项目)在 this github issue 中描述。

我想出了如何使用 romac 的 cargo-build-deps.

分支使它也能与 cargo workspaces 一起工作

此示例有 my_app 和两个工作区:utilsdb

FROM rust:nightly as rust

# Cache deps
WORKDIR /app
RUN sudo chown -R rust:rust .
RUN USER=root cargo new myapp

# Install cache-deps
RUN cargo install --git https://github.com/romac/cargo-build-deps.git

WORKDIR /app/myapp
RUN mkdir -p db/src/ utils/src/

# Copy the Cargo tomls
COPY myapp/Cargo.toml myapp/Cargo.lock ./
COPY myapp/db/Cargo.toml ./db/
COPY myapp/utils/Cargo.toml ./utils/

# Cache the deps
RUN cargo build-deps

# Copy the src folders
COPY myapp/src ./src/
COPY myapp/db/src ./db/src/
COPY myapp/utils/src/ ./utils/src/

# Build for debug
RUN cargo build