Introduction

I’ve recently been playing with some Rust projects, and one of the biggest struggles I had was getting a small platform agnostic Docker image that I could use to build any platform using docker buildx.

The code

I wrote a very small program that spits out a random number along with the current architecture to use as an sample for our Docker work.

use rand::Rng;
use current_platform::CURRENT_PLATFORM;

fn main() {
    let mut rng = rand::thread_rng();
    let r_num: u8 = rng.gen();

    println!("My random number is: {} | {}", r_num, CURRENT_PLATFORM);
}

To compile this code we use cargo build --release, this puts our binary in the target/release/ folder.

Initial Dockerfile

FROM rust:1.67 as builder

WORKDIR /build

COPY . .

RUN cargo build --release
RUN mv /build/target/release/rust-random-number-generator /

FROM scratch

WORKDIR /app

COPY --from=builder /rust-random-number-generator /app

CMD ["./rust-random-number-generator"]

Running this with a docker buildx build --push --platform linux/amd64,linux/arm64 -t <yourimage>:v0.1 . works perfectly fine, it builds and pushes the images to the reposity of your choice. Perfect!

The Issue

While our above program works locally, and will work on any architecture supported by Rust if we build it using cargo build --release on those architecutre machines, trying to run this on any arch other than the host arch results in errors.

Why? Because we need to build static binaries where Rust defaults to dynamic ones. In a scratch image we don’t have a lot of the underlying services a dynamic application can use, so we have to package it ourselves.

Improved Dockerfile

FROM rust:1.67 as builder

WORKDIR /build

COPY . .

RUN RUSTFLAGS="-C target-feature=+crt-static" cargo build --release
RUN mv /build/target/release/rust-random-number-generator /

FROM scratch

WORKDIR /app

COPY --from=builder /rust-random-number-generator /app

CMD ["./rust-random-number-generator"]

With the addition of RUSTFLAGS=-C target-feature=+crt-static we’ve specified we want to create static binaries for the architecture being built.

A more complex example

The above program and dockerfile now work without issue, but this was a very simple setup. What if we use a more complex one?

For a more complex example I decided to use one of the Helium Oracles

Our updated Dockerfile looks like this:

FROM rust:1.67 as builder

WORKDIR /build

COPY . .

RUN RUSTFLAGS="-C target-feature=+crt-static" cargo build -p ingest --release
RUN mv /build/target/release/ingest /

FROM scratch

WORKDIR /app

COPY --from=builder /ingest /app

CMD ["./ingest"]

The second error

Running our more complex program in the same way as our first example program results in the following error:

#0 26.94 error: cannot produce proc-macro for `async-stream-impl v0.3.3` as the target `aarch64-unknown-linux-gnu` does not support these crate types

Which is odd because we can compile these programs on both amd64, and arm64 outside of Docker without issue. I tested both.

I spent a good bit of time debugging here and it seems the root cause is when you specify the build flags RUSTFLAGS="-C target-feature=+crt-static" Rust will default to building a static binary for the host architecture, not the Docker container architecture. So attempting to natively compile for the <host arch> while on the <container arch> and we end in an error.

We can confirm this is the case by adding a specific architecture with the --target parameter and testing. Now that we know how to make it work, how do we make it architecture agnostic again while using the --target parameter?

The solution

The magic of rustc -vV. With a little bit of shell scripting we can get the current architecture of our container doing the complation: $(rustc -vV | sed -n 's|host: ||p')

Our updated Dockerfile:

FROM rust:1.67 as builder

WORKDIR /build

COPY . .

RUN echo "$(rustc -vV | sed -n 's|host: ||p')" > rust_target

RUN RUSTFLAGS="-C target-feature=+crt-static" cargo build --target $(cat rust_target) -p ingest --release
RUN mv /build/target/$(cat rust_target)/release/ingest /

FROM scratch

WORKDIR /app

COPY --from=builder /ingest /app

CMD ["./ingest"]

Now we’re able to use the RUSTFLAGS to compile a static binary, and we’re specifying the proper architecture dynamically based on the container doing the compiling.

Building this with docker buildx build --push --platform linux/amd64,linux/arm64 -t <image>:v0.2 works without issues. You can pull the image locally and run on either of those architectures.

This may not work everywhere. Just as the first program we had working without needing --target, there may be more complex programs out there that need additional tweaking to get working right. For now though, I’m pretty happy with getting this finished and off my to-do/learning list.