使用 Docker 构建缓存 Rust 依赖项
Cache Rust dependencies with Docker build
我在 Rust + Actix-web 中有一个 hello world web 项目。我有几个问题。首先是代码的每次更改都会导致重新编译整个项目,包括下载和编译每个板条箱。我想像正常开发一样工作——这意味着缓存编译的箱子并且只重新编译我的代码库。第二个问题是它没有公开我的应用程序。无法通过网络浏览器访问
Docker 文件:
FROM rust
WORKDIR /var/www/app
COPY . .
EXPOSE 8080
RUN cargo run
docker-compose.yml:
version: "3"
services:
app:
container_name: hello-world
build: .
ports:
- '8080:8080'
volumes:
- .:/var/www/app
- registry:/root/.cargo/registry
volumes:
registry:
driver: local
main.rs:
extern crate actix_web;
use actix_web::{web, App, HttpServer, Responder};
fn index() -> impl Responder {
"Hello world"
}
fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(web::resource("/").to(index)))
.bind("0.0.0.0:8080")?
.run()
}
Cargo.toml:
[package]
name = "hello-world"
version = "0.1.0"
authors = []
edition = "2018"
[dependencies]
actix-web = "1.0"
似乎您并不是唯一一个通过 docker 构建过程缓存 Rust 依赖项的人。这是一篇很棒的文章,可以帮助您一路走来:https://blog.mgattozzi.dev/caching-rust-docker-builds/
它的要点是你首先需要一个 dummy.rs 和你的 Cargo.toml,然后构建它来缓存依赖项,然后稍后复制你的应用程序源,以免每次都使缓存无效建造。
Dockerfile
FROM rust
WORKDIR /var/www/app
COPY dummy.rs .
COPY Cargo.toml .
RUN sed -i 's#src/main.rs#dummy.rs#' Cargo.toml
RUN cargo build --release
RUN sed -i 's#dummy.rs#src/main.rs#' Cargo.toml
COPY . .
RUN cargo build --release
CMD ["target/release/app"]
CMD 应用程序名称 "app" 基于您在 Cargo.toml 中为您的二进制文件指定的名称。
dummy.rs
fn main() {}
Cargo.toml
[package]
name = "app"
version = "0.1.0"
authors = ["..."]
[[bin]]
name = "app"
path = "src/main.rs"
[dependencies]
actix-web = "1.0.0"
src/main.rs
extern crate actix_web;
use actix_web::{web, App, HttpServer, Responder};
fn index() -> impl Responder {
"Hello world"
}
fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(web::resource("/").to(index)))
.bind("0.0.0.0:8080")?
.run()
}
我认为问题是您的 volumes
定义没有进行绑定安装。我相信您当前的配置是将 HOST ./registry/
复制到 DOCKER /root/.cargo/registry/
,写入 DOCKER /root/.cargo/registry/
,并在容器关闭时丢弃内容。
相反,您需要在卷上指定 bind
类型:
version: "3"
services:
app:
container_name: hello-world
build: .
environment:
- CARGO_HOME=/var/www/
ports:
- '8080:8080'
volumes:
- .:/var/www/app
- type: bind
source: ./registry
target: /root/.cargo/registry
但是,请记住还会创建一个 /root/.cargo/.package-cache
文件,但不会保留在此处。相反,您可以将 source
更改为 ./.cargo
并将目标更改为 /root/.cargo
.
对于我自己的(主要是 cli)rust 项目,我喜欢使用 drop-in replacement I've written for cargo
我已经确认在构建之间缓存包,大大减少了构建时间。这可以复制到 /usr/local/bin
以在全局使用,或 运行 作为 ./cargo build
在单个项目中使用。但请记住,此特定脚本假定应用程序位于容器内的 /usr/src/app
,因此可能需要根据您的使用进行调整。
借助(仍处于实验阶段)Docker Buildkit,您最终可以在 docker build
步骤中正确缓存构建文件夹:
Docker文件:
# syntax=docker/dockerfile:experimental
from rust
ENV HOME=/home/root
WORKDIR $HOME/app
[...]
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/home/root/app/target \
cargo build --release
然后运行:
DOCKER_BUILDKIT=1 docker build . --progress=plain
后续 docker 构建将重用缓存中的 cargo 和 target 文件夹,从而大大加快构建速度。
要清除 docker 缓存装载:docker builder prune --filter type=exec.cachemount
如果您没有看到正确的缓存:如果您没有看到正确的缓存,请务必确认 cargo/registry 和目标文件夹在 docker 图像中的位置。
最小工作示例:https://github.com/benmarten/sccache-docker-test/tree/no-sccache
这就是我所做的,它与构建脚本兼容。这是一个多阶段构建,因此它会生成一个小图像,但会在第一个图像中缓存构建的依赖项。
FROM rust:1.43 AS builder
RUN apt-get update
RUN cd /tmp && USER=root cargo new --bin <projectname>
WORKDIR /tmp/<projectname>
# cache rust dependencies in docker layer
COPY Cargo.toml Cargo.lock ./
RUN touch build.rs && echo "fn main() {println!(\"cargo:rerun-if-changed=\\"/tmp/<projectname>/build.rs\\"\");}" >> build.rs
RUN cargo build --release
# build the real stuff and disable cache via the ADD
ADD "https://www.random.org/cgi-bin/randbyte?nbytes=10&format=h" skipcache
COPY ./build.rs ./build.rs
# force the build.rs script to run by modifying it
RUN echo " " >> build.rs
COPY ./src ./src
RUN cargo build --release
FROM rust:1.43
WORKDIR /bin
COPY --from=builder /tmp/<projectname>/target/release/server /bin/<project binary>
RUN chmod +x ./<project binary>
CMD ./<project binary>
虽然 electronix384128 回答非常好。我想通过为 .cargo/git
添加缓存来扩展它,这是使用 git 的任何依赖项所需的,并通过添加多阶段 docker 示例。
使用 rust-musl-builder 和 Docker Buildkit 功能,现在默认为 Docker Desktop 2.4。在其他版本上,您可能仍需要通过以下方式启用它:DOCKER_BUILDKIT=1 docker build .
rusl-musl-builder
的工作目录是 /home/rust/src
尝试在 --mount
上设置 uid/gid 但由于目标中的权限问题未能编译 rust。
# syntax=docker/dockerfile:1.2
FROM ekidd/rust-musl-builder:stable AS builder
COPY . .
RUN --mount=type=cache,target=/home/rust/.cargo/git \
--mount=type=cache,target=/home/rust/.cargo/registry \
--mount=type=cache,sharing=private,target=/home/rust/src/target \
sudo chown -R rust: target /home/rust/.cargo && \
cargo build --release && \
# Copy executable out of the cache so it is available in the final image.
cp target/x86_64-unknown-linux-musl/release/my-executable ./my-executable
FROM alpine
COPY --from=builder /home/rust/src/my-executable .
USER 1000
CMD ["./my-executable"]
您可以使用 cargo-chef
通过 multi-stage 构建利用 Docker 层缓存。
FROM rust as planner
WORKDIR app
# We only pay the installation cost once,
# it will be cached from the second build onwards
RUN cargo install cargo-chef
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM rust as cacher
WORKDIR app
RUN cargo install cargo-chef
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
FROM rust as builder
WORKDIR app
COPY . .
# Copy over the cached dependencies
COPY --from=cacher /app/target target
RUN cargo build --release --bin app
FROM rust as runtime
WORKDIR app
COPY --from=builder /app/target/release/app /usr/local/bin
ENTRYPOINT ["./usr/local/bin/app"]
它不需要 Buildkit,适用于简单的项目和工作区。
您可以在 release announcement.
中找到更多详细信息
根据 的回答,您可以 RUN echo "fn main() {}" > ./src/main.rs
在构建应用程序之前构建依赖项。
首先只复制您的 Cargo.toml
和 Cargo.lock
文件并构建虚拟 main.rs 文件:
FROM rust as rust-builder
WORKDIR /usr/src/app
# Copy Cargo files
COPY ./Cargo.toml .
COPY ./Cargo.lock .
# Create fake main.rs file in src and build
RUN mkdir ./src && echo 'fn main() { println!("Dummy!"); }' > ./src/main.rs
RUN cargo build --release
然后你可以复制你真正的 src 目录并运行再次构建:
# Copy source files over
RUN rm -rf ./src
COPY ./src ./src
# The last modified attribute of main.rs needs to be updated manually,
# otherwise cargo won't rebuild it.
RUN touch -a -m ./src/main.rs
RUN cargo build --release
然后我们可以将文件复制到精简版的debain。
这是完整的 docker 文件:
FROM rust as rust-builder
WORKDIR /usr/src/app
COPY ./Cargo.toml .
COPY ./Cargo.lock .
RUN mkdir ./src && echo 'fn main() { println!("Dummy!"); }' > ./src/main.rs
RUN cargo build --release
RUN rm -rf ./src
COPY ./src ./src
RUN touch -a -m ./src/main.rs
RUN cargo build --release
FROM debian:buster-slim
COPY --from=rust-builder /usr/src/app/target/release/app /usr/local/bin/
WORKDIR /usr/local/bin
CMD ["app"]
我遇到了与您完全相同的问题,并尝试了多种方法来通过缓存依赖项来缩短构建时间。
1。 回答
它完成了工作,并且 easy-to-understand 解释了它的工作原理,这是一个很好的解决方案。
然而,这归结为偏好,但如果您不以这种方式缓存依赖项,则可以按照#2.
2。使用 cargo-chef
,创建者本人,完成了使用 cargo-chef
的必要步骤,但这里是 github 页面中的一个*略微调整的示例.
Dockerfile
FROM lukemathwalker/cargo-chef:latest-rust-1.60.0 AS chef
WORKDIR /app
FROM chef as planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM chef as builder
COPY --from=planner /app/recipe.json recipe.json
# Build the dependencies (and add to docker's caching layer)
# This caches the dependency files similar to how @ckaserer's solution
# does, but is handled solely through the `cargo-chef` library.
RUN cargo chef cook --release --recipe-path recipe.json
# Build the application
COPY . .
RUN cargo build --release --bin emailer
FROM debian:buster-slim AS runtime
WORKDIR /app
COPY --from=builder /app/target/release/<Name of Rust Application> /usr/local/bin
ENTRYPOINT ["/usr/local/bin/<Name of Rust Application>"]
您应该注意到上述更改后构建时间显着减少!
旁注,据我所知,this blog entry 虽然不是在 dockerized 构建中,但包含有关在本地计算机上更快地编译 Rust 应用程序的最佳信息。您可能会发现它有帮助,所以如果您有兴趣,我建议您检查一下。
我在 Rust + Actix-web 中有一个 hello world web 项目。我有几个问题。首先是代码的每次更改都会导致重新编译整个项目,包括下载和编译每个板条箱。我想像正常开发一样工作——这意味着缓存编译的箱子并且只重新编译我的代码库。第二个问题是它没有公开我的应用程序。无法通过网络浏览器访问
Docker 文件:
FROM rust
WORKDIR /var/www/app
COPY . .
EXPOSE 8080
RUN cargo run
docker-compose.yml:
version: "3"
services:
app:
container_name: hello-world
build: .
ports:
- '8080:8080'
volumes:
- .:/var/www/app
- registry:/root/.cargo/registry
volumes:
registry:
driver: local
main.rs:
extern crate actix_web;
use actix_web::{web, App, HttpServer, Responder};
fn index() -> impl Responder {
"Hello world"
}
fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(web::resource("/").to(index)))
.bind("0.0.0.0:8080")?
.run()
}
Cargo.toml:
[package]
name = "hello-world"
version = "0.1.0"
authors = []
edition = "2018"
[dependencies]
actix-web = "1.0"
似乎您并不是唯一一个通过 docker 构建过程缓存 Rust 依赖项的人。这是一篇很棒的文章,可以帮助您一路走来:https://blog.mgattozzi.dev/caching-rust-docker-builds/
它的要点是你首先需要一个 dummy.rs 和你的 Cargo.toml,然后构建它来缓存依赖项,然后稍后复制你的应用程序源,以免每次都使缓存无效建造。
Dockerfile
FROM rust
WORKDIR /var/www/app
COPY dummy.rs .
COPY Cargo.toml .
RUN sed -i 's#src/main.rs#dummy.rs#' Cargo.toml
RUN cargo build --release
RUN sed -i 's#dummy.rs#src/main.rs#' Cargo.toml
COPY . .
RUN cargo build --release
CMD ["target/release/app"]
CMD 应用程序名称 "app" 基于您在 Cargo.toml 中为您的二进制文件指定的名称。
dummy.rs
fn main() {}
Cargo.toml
[package]
name = "app"
version = "0.1.0"
authors = ["..."]
[[bin]]
name = "app"
path = "src/main.rs"
[dependencies]
actix-web = "1.0.0"
src/main.rs
extern crate actix_web;
use actix_web::{web, App, HttpServer, Responder};
fn index() -> impl Responder {
"Hello world"
}
fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(web::resource("/").to(index)))
.bind("0.0.0.0:8080")?
.run()
}
我认为问题是您的 volumes
定义没有进行绑定安装。我相信您当前的配置是将 HOST ./registry/
复制到 DOCKER /root/.cargo/registry/
,写入 DOCKER /root/.cargo/registry/
,并在容器关闭时丢弃内容。
相反,您需要在卷上指定 bind
类型:
version: "3"
services:
app:
container_name: hello-world
build: .
environment:
- CARGO_HOME=/var/www/
ports:
- '8080:8080'
volumes:
- .:/var/www/app
- type: bind
source: ./registry
target: /root/.cargo/registry
但是,请记住还会创建一个 /root/.cargo/.package-cache
文件,但不会保留在此处。相反,您可以将 source
更改为 ./.cargo
并将目标更改为 /root/.cargo
.
对于我自己的(主要是 cli)rust 项目,我喜欢使用 drop-in replacement I've written for cargo
我已经确认在构建之间缓存包,大大减少了构建时间。这可以复制到 /usr/local/bin
以在全局使用,或 运行 作为 ./cargo build
在单个项目中使用。但请记住,此特定脚本假定应用程序位于容器内的 /usr/src/app
,因此可能需要根据您的使用进行调整。
借助(仍处于实验阶段)Docker Buildkit,您最终可以在 docker build
步骤中正确缓存构建文件夹:
Docker文件:
# syntax=docker/dockerfile:experimental
from rust
ENV HOME=/home/root
WORKDIR $HOME/app
[...]
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/home/root/app/target \
cargo build --release
然后运行:
DOCKER_BUILDKIT=1 docker build . --progress=plain
后续 docker 构建将重用缓存中的 cargo 和 target 文件夹,从而大大加快构建速度。
要清除 docker 缓存装载:docker builder prune --filter type=exec.cachemount
如果您没有看到正确的缓存:如果您没有看到正确的缓存,请务必确认 cargo/registry 和目标文件夹在 docker 图像中的位置。
最小工作示例:https://github.com/benmarten/sccache-docker-test/tree/no-sccache
这就是我所做的,它与构建脚本兼容。这是一个多阶段构建,因此它会生成一个小图像,但会在第一个图像中缓存构建的依赖项。
FROM rust:1.43 AS builder
RUN apt-get update
RUN cd /tmp && USER=root cargo new --bin <projectname>
WORKDIR /tmp/<projectname>
# cache rust dependencies in docker layer
COPY Cargo.toml Cargo.lock ./
RUN touch build.rs && echo "fn main() {println!(\"cargo:rerun-if-changed=\\"/tmp/<projectname>/build.rs\\"\");}" >> build.rs
RUN cargo build --release
# build the real stuff and disable cache via the ADD
ADD "https://www.random.org/cgi-bin/randbyte?nbytes=10&format=h" skipcache
COPY ./build.rs ./build.rs
# force the build.rs script to run by modifying it
RUN echo " " >> build.rs
COPY ./src ./src
RUN cargo build --release
FROM rust:1.43
WORKDIR /bin
COPY --from=builder /tmp/<projectname>/target/release/server /bin/<project binary>
RUN chmod +x ./<project binary>
CMD ./<project binary>
虽然 electronix384128 回答非常好。我想通过为 .cargo/git
添加缓存来扩展它,这是使用 git 的任何依赖项所需的,并通过添加多阶段 docker 示例。
使用 rust-musl-builder 和 Docker Buildkit 功能,现在默认为 Docker Desktop 2.4。在其他版本上,您可能仍需要通过以下方式启用它:DOCKER_BUILDKIT=1 docker build .
rusl-musl-builder
的工作目录是 /home/rust/src
尝试在 --mount
上设置 uid/gid 但由于目标中的权限问题未能编译 rust。
# syntax=docker/dockerfile:1.2
FROM ekidd/rust-musl-builder:stable AS builder
COPY . .
RUN --mount=type=cache,target=/home/rust/.cargo/git \
--mount=type=cache,target=/home/rust/.cargo/registry \
--mount=type=cache,sharing=private,target=/home/rust/src/target \
sudo chown -R rust: target /home/rust/.cargo && \
cargo build --release && \
# Copy executable out of the cache so it is available in the final image.
cp target/x86_64-unknown-linux-musl/release/my-executable ./my-executable
FROM alpine
COPY --from=builder /home/rust/src/my-executable .
USER 1000
CMD ["./my-executable"]
您可以使用 cargo-chef
通过 multi-stage 构建利用 Docker 层缓存。
FROM rust as planner
WORKDIR app
# We only pay the installation cost once,
# it will be cached from the second build onwards
RUN cargo install cargo-chef
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM rust as cacher
WORKDIR app
RUN cargo install cargo-chef
COPY --from=planner /app/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
FROM rust as builder
WORKDIR app
COPY . .
# Copy over the cached dependencies
COPY --from=cacher /app/target target
RUN cargo build --release --bin app
FROM rust as runtime
WORKDIR app
COPY --from=builder /app/target/release/app /usr/local/bin
ENTRYPOINT ["./usr/local/bin/app"]
它不需要 Buildkit,适用于简单的项目和工作区。 您可以在 release announcement.
中找到更多详细信息根据 RUN echo "fn main() {}" > ./src/main.rs
在构建应用程序之前构建依赖项。
首先只复制您的 Cargo.toml
和 Cargo.lock
文件并构建虚拟 main.rs 文件:
FROM rust as rust-builder
WORKDIR /usr/src/app
# Copy Cargo files
COPY ./Cargo.toml .
COPY ./Cargo.lock .
# Create fake main.rs file in src and build
RUN mkdir ./src && echo 'fn main() { println!("Dummy!"); }' > ./src/main.rs
RUN cargo build --release
然后你可以复制你真正的 src 目录并运行再次构建:
# Copy source files over
RUN rm -rf ./src
COPY ./src ./src
# The last modified attribute of main.rs needs to be updated manually,
# otherwise cargo won't rebuild it.
RUN touch -a -m ./src/main.rs
RUN cargo build --release
然后我们可以将文件复制到精简版的debain。 这是完整的 docker 文件:
FROM rust as rust-builder
WORKDIR /usr/src/app
COPY ./Cargo.toml .
COPY ./Cargo.lock .
RUN mkdir ./src && echo 'fn main() { println!("Dummy!"); }' > ./src/main.rs
RUN cargo build --release
RUN rm -rf ./src
COPY ./src ./src
RUN touch -a -m ./src/main.rs
RUN cargo build --release
FROM debian:buster-slim
COPY --from=rust-builder /usr/src/app/target/release/app /usr/local/bin/
WORKDIR /usr/local/bin
CMD ["app"]
我遇到了与您完全相同的问题,并尝试了多种方法来通过缓存依赖项来缩短构建时间。
1。 回答
它完成了工作,并且 easy-to-understand 解释了它的工作原理,这是一个很好的解决方案。 然而,这归结为偏好,但如果您不以这种方式缓存依赖项,则可以按照#2.
2。使用 cargo-chef
cargo-chef
的必要步骤,但这里是 github 页面中的一个*略微调整的示例.
Dockerfile
FROM lukemathwalker/cargo-chef:latest-rust-1.60.0 AS chef
WORKDIR /app
FROM chef as planner
COPY . .
RUN cargo chef prepare --recipe-path recipe.json
FROM chef as builder
COPY --from=planner /app/recipe.json recipe.json
# Build the dependencies (and add to docker's caching layer)
# This caches the dependency files similar to how @ckaserer's solution
# does, but is handled solely through the `cargo-chef` library.
RUN cargo chef cook --release --recipe-path recipe.json
# Build the application
COPY . .
RUN cargo build --release --bin emailer
FROM debian:buster-slim AS runtime
WORKDIR /app
COPY --from=builder /app/target/release/<Name of Rust Application> /usr/local/bin
ENTRYPOINT ["/usr/local/bin/<Name of Rust Application>"]
您应该注意到上述更改后构建时间显着减少!
旁注,据我所知,this blog entry 虽然不是在 dockerized 构建中,但包含有关在本地计算机上更快地编译 Rust 应用程序的最佳信息。您可能会发现它有帮助,所以如果您有兴趣,我建议您检查一下。