【问题标题】:Cache Rust dependencies with Docker build使用 Docker 构建缓存 Rust 依赖项
【发布时间】:2022-05-16 22:16:02
【问题描述】:

我在 Rust + Actix-web 中有 hello world web 项目。我有几个问题。首先是代码的每次更改都会导致重新编译整个项目,包括下载和编译每个 crate。我想像在正常开发中一样工作 - 这意味着缓存已编译的 crate 并且只重新编译我的代码库。第二个问题是它不会暴露我的应用程序。无法通过网络浏览器访问

Dockerfile:

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"

【问题讨论】:

  • 请同时提供您的 cargo.toml
  • 您的 Dockerfile 缺少 CMD 行;容器启动时应该运行什么? (构建序列真的完成了吗?)
  • 如果答案解决了您的问题,请接受它或提供其他信息以进一步调试手头的问题

标签: docker rust dockerfile actix-web


【解决方案1】:

似乎你并不是唯一一个努力通过 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()
}

【讨论】:

  • 很好的答案。作为参考,COPY dummy.rs 可以替换为RUN echo "fn main() {}" &gt; dummy.rs,这样可以避免在存储库中出现文件dummy.rs
  • 您可以添加第二个[[bin]] 目标,而不是操纵Cargo.toml。确保将src/dummy.rs 与您的Cargo.toml 一起复制。然后添加第二个 bin 目标(如示例中的第 5-7 行)。设置name= "download-only"path = "src/dummy.rs" 并调用cargo build --bin download-only
  • 如果你正在做RUN echo "fn main() {}" &gt; ./src/main.rs,然后COPY ./src ./src,cargo 可能不会重建你的应用程序。您需要更新 main.rs 文件的最后修改,以通知 cargo 重建它。在您的第二次货物构建之前,您需要运行 RUN touch -a -m ./src/main.rs 来更新文件的最后修改。
  • @MartinSommer [[bin]] 方法不起作用。对于你的二进制文件download-only,你需要cargo build --bin download-only,这将使docker缓存依赖于这个命令。但是,您仍然需要cargo build --bin app,它不会被缓存。 docker正确缓存的唯一方法是如果两个构建命令完全相同,所以我认为没有其他方法可以操作Cargo.toml
  • 我发现使用 --offline 标志也很有用,以确保 cargo 不会尝试刷新其依赖项作为标准构建的一部分。虽然出于某种原因我使用 cargo install --offline --path . 而不是 build 但我认为它也适用于构建。
【解决方案2】:

使用(仍处于试验阶段)Docker Buildkit,您终于可以在 docker build 步骤中正确缓存构建文件夹:

Dockerfile:

# 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

【讨论】:

  • 不会使用--mount=type=cache,target=/home/rust/src/target 意味着其他同时构建访问target 目录?是否应该将sharing=privatesharing=locked 添加到该缓存行?也许结合一个单独的层来从缓存中填充构建本地 target/ 和另一个层来将新的东西推回缓存?
【解决方案3】:

您可以使用 cargo-chef 通过多阶段构建来利用 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找到更多详细信息。

【讨论】:

  • 不会使用--mount=type=cache,target=/home/rust/src/target 意味着其他同时构建访问target 目录?是否应该将sharing=privatesharing=locked 添加到该缓存行?也许结合一个单独的层从缓存中填充构建本地 target/ 和另一个层将新的东西推回缓存?
  • 我收到此错误:docker: Error response from daemon: OCI runtime create failed: container_linux.go:370: starting container process caused: exec: "./usr/local/bin/app": stat ./usr/local/bin/app: no such file or directory: unknown.
  • 更新:我通过删除“ENTRYPOINT”数组值中的点来修复错误。
  • 这是一个很好的答案。但是它缺少 crates.io 的缓存索引,并且复制大目标目录需要很长时间
【解决方案4】:

虽然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"]

【讨论】:

  • 不会使用--mount=type=cache,target=/home/rust/src/target 意味着其他同时构建访问target 目录?是否应该将sharing=privatesharing=locked 添加到该缓存行?也许结合一个单独的层从缓存中填充构建本地 target/ 和另一个层将新的东西推回缓存?
  • 感谢您的提示,我已经更新了示例并且 dockerfile 语法现在不再是实验性的?
【解决方案5】:

根据@ckaserer 的回答,您可以RUN echo "fn main() {}" &gt; ./src/main.rs 在构建您的应用程序之前构建依赖项。

首先复制您的 Cargo.tomlCargo.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"]

【讨论】:

  • 就我而言,在RUN rm -rf ./src 之后我还需要添加RUN rm -rf ./target/release
【解决方案6】:

我认为问题在于您的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,因此可能需要根据您的使用进行调整。

【讨论】:

    【解决方案7】:

    这就是我所做的,它与构建脚本兼容。这是一个多阶段构建,因此它会生成一个小图像,但会将构建的依赖项缓存在第一个图像中。

    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>
    

    【讨论】:

      【解决方案8】:

      我遇到了和你完全相同的问题,并尝试了多种方法来通过缓存依赖项来缩短构建时间。

      1。 @ckaserer's回答

      它可以完成工作,并且通过易于理解的解释来解释它的工作原理,这是一个很好的解决方案。 但是,这归结为偏好,但如果您不以这种方式缓存依赖项,则可以遵循 #2。

      2。使用cargo-chef

      @LukeMathWalker,创建者本人,完成了使用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 应用程序的最佳信息。你可能会觉得它很有帮助,所以如果你有兴趣,我建议你去看看。

      【讨论】:

        猜你喜欢
        • 2014-11-10
        • 2022-12-18
        • 2019-06-30
        • 2019-03-24
        • 1970-01-01
        • 1970-01-01
        • 2017-07-01
        • 2019-07-23
        • 2020-03-19
        相关资源
        最近更新 更多