Skip to content

Build a Rust Application

Target audience: Developers Goal: Build a reproducible Rust binary with external crate dependencies using StageX.

Prerequisites

Create the Project

We'll build jsonfmt — a JSON pretty-printer that reads from stdin and outputs formatted JSON.

mkdir jsonfmt && cd jsonfmt

Cargo.toml

[package]
name = "jsonfmt"
version = "0.1.0"
edition = "2021"

[dependencies]
serde_json = "1"

src/main.rs

use std::io::Read;

fn main() {
    let mut input = String::new();
    std::io::stdin().read_to_string(&mut input).unwrap();
    let value: serde_json::Value = serde_json::from_str(&input).unwrap();
    let pretty = serde_json::to_string_pretty(&value).unwrap();
    println!("{}", pretty);
}

Write the Containerfile

Create Containerfile:

FROM docker.io/stagex/pallet-rust@sha256:2fbe7b164dd92edb9c1096152f6d27592d8a69b1b8eb2fc907b5fadea7d11668 AS build
WORKDIR /app
COPY Cargo.toml ./
COPY src/ ./src/

RUN cargo fetch

RUN --network=none <<EOF
    ARCH="$(uname -m)"
    RUSTFLAGS="-C target-feature=+crt-static" \
    cargo build \
        --frozen \
        --release \
        --target "${ARCH}-unknown-linux-musl" \
        --bin jsonfmt
    cp "target/${ARCH}-unknown-linux-musl/release/jsonfmt" /jsonfmt
EOF

FROM scratch
COPY --from=build /jsonfmt /jsonfmt
ENTRYPOINT ["/jsonfmt"]

Key points:

  • RUN cargo fetch — Downloads crate dependencies. This is the only step that touches the network.
  • RUN --network=none — The actual compilation is hermetic. All crates were already fetched.
  • ARCH="$(uname -m)" — Detects the build architecture automatically. The Containerfile works on amd64 (x86_64) and arm64 (aarch64) hosts without changes.
  • --frozen — Prevents modification of Cargo.lock, pinning exact dependency versions for reproducibility.
  • RUSTFLAGS="-C target-feature=+crt-static" — Produces a fully static binary with no runtime library dependencies.

FROM scratch vs core-filesystem: Use FROM scratch for the smallest possible image. If your app needs filesystem content (e.g. /etc/passwd, CA certificates, timezone data), replace FROM scratch with FROM stagex/core-filesystem.

Existing Cargo.lock: If your project already has a Cargo.lock (e.g. from cargo init or a previous build), add COPY Cargo.lock ./ right after COPY Cargo.toml ./. This ensures cargo fetch starts from the same resolved versions. Without it, cargo fetch generates the lockfile from scratch — which produces the same result but takes longer.

Custom Cargo Registry

If your project depends on crates from a private or alternative registry, configure it inline before the fetch step:

ADD <<EOF /.cargo/config.toml
[registries.internal]
index = "https://git.example.com/cargo-index.git"
EOF

The ADD must appear before RUN cargo fetch so the registry is available during dependency resolution.

Build

podman build --timestamp 1 -t jsonfmt .

Output:

[1/2] STEP 5/6: RUN cargo fetch
    Updating crates.io index
     Downloading crates ...
    Downloaded serde_json v1.0.149
    ...
[1/2] STEP 6/6: RUN --network=none ...
    Compiling serde_json v1.0.149
    Compiling jsonfmt v0.1.0 (/app)
    Finished `release` profile [optimized]
[2/2] COMMIT jsonfmt

Run

echo '{"name":"StageX","version":1}' | podman run --rm -i jsonfmt

Output:

{
  "name": "StageX",
  "version": 1
}

Verify Reproducibility

The --timestamp 1 flag normalizes filesystem and image metadata timestamps to epoch, ensuring the image digest is deterministic. Rebuild with --no-cache and compare digests:

podman build --no-cache --timestamp 1 -t jsonfmt:rebuild .
podman inspect jsonfmt --format '{{.Digest}}'
podman inspect jsonfmt:rebuild --format '{{.Digest}}'

Both digests will be identical — the pinned toolchain, SOURCE_DATE_EPOCH, and --frozen dependency pinning guarantee deterministic outputs. See the Quick Start tutorial for a detailed walkthrough.

Cross-Compile for a Different Architecture

Build for linux/arm64 from an amd64 machine using podman build --platform. The FROM line needs the platform qualifier:

=== "linux/amd64"

```dockerfile
FROM --platform=linux/amd64 docker.io/stagex/pallet-rust@sha256:2fbe7b164dd92edb9c1096152f6d27592d8a69b1b8eb2fc907b5fadea7d11668 AS build
```

=== "linux/arm64"

```dockerfile
FROM --platform=linux/arm64 docker.io/stagex/pallet-rust@sha256:2fbe7b164dd92edb9c1096152f6d27592d8a69b1b8eb2fc907b5fadea7d11668 AS build
```

Then build for the target:

podman build --platform linux/arm64 -t jsonfmt:arm64 .

The ARCH="$(uname -m)" pattern detects the target architecture inside the container (QEMU emulation makes uname -m return aarch64 under --platform linux/arm64).

For a single Containerfile that works across platforms, use the TARGETPLATFORM build arg:

FROM --platform=$TARGETPLATFORM docker.io/stagex/pallet-rust@sha256:2fbe7b164dd92edb9c1096152f6d27592d8a69b1b8eb2fc907b5fadea7d11668 AS build

This lets you build for any architecture with just podman build --platform ... — no manual FROM line edits needed.

See Also