Build a Rust Application
Target audience: Developers Goal: Build a reproducible Rust binary with external crate dependencies using StageX.
Prerequisites
- Completed the Quick Start tutorial
- Podman installed
- Basic familiarity with Rust and Cargo
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 ofCargo.lock, pinning exact dependency versions for reproducibility.RUSTFLAGS="-C target-feature=+crt-static"— Produces a fully static binary with no runtime library dependencies.
FROM scratchvscore-filesystem: UseFROM scratchfor the smallest possible image. If your app needs filesystem content (e.g./etc/passwd, CA certificates, timezone data), replaceFROM scratchwithFROM stagex/core-filesystem.Existing
Cargo.lock: If your project already has aCargo.lock(e.g. fromcargo initor a previous build), addCOPY Cargo.lock ./right afterCOPY Cargo.toml ./. This ensurescargo fetchstarts from the same resolved versions. Without it,cargo fetchgenerates 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
- Quick Start tutorial — prerequisite tutorial
- CI Pipeline tutorial — advanced multi-stage patterns
- Containerfile Syntax reference — syntax details
- Add a New Package how-to — contributing to StageX
- Reproduce Builds how-to — verification beyond basic digest check
- Cross-Compile for Other Architectures how-to — more on cross-compilation