Search code examples
dockercompilationrust-cargodocker-volume

Cache Cargo dependencies in a Docker volume


I'm building a Rust program in Docker (rust:1.33.0).

Every time code changes, it re-compiles (good), which also re-downloads all dependencies (bad).

I thought I could cache dependencies by adding VOLUME ["/usr/local/cargo"]. edit I've also tried moving this dir with CARGO_HOME without luck.

I thought that making this a volume would persist the downloaded dependencies, which appear to be in this directory.

But it didn't work, they are still downloaded every time. Why?


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

Built with just docker build ..

Cargo.toml

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

Code: just hello world

Output of second run after changing 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

Solution

  • A volume inside the Dockerfile is counter-productive here. That would mount an anonymous volume at each build step, and again when you run the container. The volume during each build step is discarded after that step completes, which means you would need to download the entire contents again for any other step needing those dependencies.

    The standard model for this is to copy your dependency specification, run the dependency download, copy your code, and then compile or run your code, in 4 separate steps. That lets docker cache the layers in an efficient manner. I'm not familiar with rust or cargo specifically, but I believe that would look like:

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

    Another option is to turn on some experimental features with BuildKit (available in 18.09, released 2018-11-08) so that docker saves these dependencies in what is similar to a named volume for your build. The directory can be reused across builds, but never gets added to the image itself, making it useful for things like a download cache.

    # 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"]
    

    Note that the above assumes cargo is caching files in /root/.cargo. You'd need to verify this and adjust as appropriate. I also haven't mixed the mount syntax with a json exec syntax to know if that part works. You can read more about the BuildKit experimental features here: https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/experimental.md

    Turning on BuildKit from 18.09 and newer versions is as easy as export DOCKER_BUILDKIT=1 and then running your build from that shell.