我正在Docker(rust:1.33.0
)中构建一个Rust程序。
每次代码更改时,它都会重新编译(好),这也会重新下载所有依赖项(坏)。
我以为我可以通过添加VOLUME ["/usr/local/cargo"]
来缓存依赖项。编辑我也试过用CARGO_HOME
移动这个目录没有运气。
我认为将其设为卷将保留下载的依赖项,这些依赖项似乎位于此目录中。
但它没有用,它们每次都会被下载。为什么?
Dockerfile
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
Dockerfile中的卷在这里适得其反。这将在每个构建步骤中安装匿名卷,并在运行容器时再次安装。在该步骤完成后,将丢弃每个构建步骤期间的卷,这意味着您需要再次下载整个内容以用于需要这些依赖项的任何其他步骤。
标准模型是通过4个单独的步骤复制依赖项规范,运行依赖项下载,复制代码,然后编译或运行代码。这让docker以有效的方式缓存层。我不熟悉生锈或货物,但我相信看起来像:
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(18.09中提供)打开一些实验性功能,以便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"]
请注意,上述假设货物是在/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卷来缓存依赖项。 Docker会自动缓存图像的不同“图层”。基本上,Dockerfile中的每个命令都对应于图像的一个层。您面临的问题是基于Docker
图像层缓存的工作原理。
Docker遵循的图像层缓存规则列在官方documentation中:
- 从已经在高速缓存中的父图像开始,将下一条指令与从该基本图像导出的所有子图像进行比较,以查看它们中的一个是否使用完全相同的指令构建。如果不是,则缓存无效。
- 在大多数情况下,只需将Dockerfile中的指令与其中一个子图像进行比较即可。但是,某些说明需要更多的检查和解释。
- 对于ADD和COPY指令,将检查映像中文件的内容,并为每个文件计算校验和。在这些校验和中不考虑文件的最后修改时间和最后访问时间。在高速缓存查找期间,将校验和与现有映像中的校验和进行比较。如果文件中的任何内容(例如内容和元数据)发生了任何更改,则缓存将失效。
- 除了ADD和COPY命令之外,高速缓存检查不会查看容器中的文件以确定高速缓存匹配。例如,在处理RUN apt-get -y update命令时,不检查容器中更新的文件以确定是否存在缓存命中。在这种情况下,只需使用命令字符串本身来查找匹配项。
缓存无效后,所有后续Dockerfile命令都会生成新映像,并且不使用缓存。
所以问题在于命令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
发生变化时才会重新安装依赖项。
希望这可以帮助。
我会说,更好的解决方案是采用码头multi-stage build作为尖头here和there
这样你就可以自己创建第一个图像,它可以构建你的应用程序和你的依赖项,然后在第二个图像中只使用第一个图像中的依赖文件夹
这是受到你对@Jack Gore's answer的评论以及上面链接的两个问题评论的启发。
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
编辑2:这里是可能性的概述。向下滚动查看原始答案。
main.rs
/ lib.rs
,然后编译依赖项。然后删除假源并添加真实源。 [缓存依赖项,但有几个带工作区的虚假文件]main.rs
/ lib.rs
,然后编译依赖项。然后创建一个包含依赖项的新层,并从那里继续。 [与上面类似]--mount
]RUN --mount=type=cache,target=/the/path cargo build
。 [缓存一切,似乎是一种好方法,但目前对我来说太新了。可执行文件不是图像的一部分]VOLUME ["/the/path"]
不起作用,这只是每层(每个命令)。注意:可以在Dockerfile中设置CARGO_HOME
和ENV CARGO_TARGET_DIR
来控制下载缓存和编译输出的位置。
另请注意:cargo fetch
至少可以缓存依赖项的下载,但不能编译。
货物工作区必须手动添加每个货物文件,而对于某些解决方案,必须生成十几个假main.rs
/ lib.rs
。对于具有单个Cargo文件的项目,解决方案可以更好地工作。
我已经通过添加缓存来为我的特定情况工作
ENV CARGO_HOME /code/dockerout/cargo
ENV CARGO_TARGET_DIR /code/dockerout/target
其中/code
是我挂载代码的目录。
这是从外部安装的,而不是从Dockerfile安装的。
EDIT1:我很困惑,为什么这样做,但是@ b.enoit.be和@BMitch清除了它,因为在Dockerfile中声明的卷只存在一层(一个命令)。