When your host machine isn't the target
You are on a MacBook. You need a binary for a Raspberry Pi. You try cargo build --target aarch64-unknown-linux-gnu. The compiler rejects you with a linker error. You spend three hours downloading sysroots, wrestling with environment variables, and fighting glibc version mismatches. You give up.
cross stops this pain. It wraps your build in a Docker container that already has the correct toolchain, linker, and system libraries for your target. You run one command. The container handles the heavy lifting. You get a working binary.
Stop fighting sysroots. Install cross.
How cross removes the friction
Cross-compilation means compiling code on machine A to run on machine B. Rust handles this well because the compiler is multi-platform. The hard part is not the compiler. The hard part is the toolchain.
To build for a different architecture, you need a C compiler, a linker, and the standard library headers for that target. Gathering these tools is tedious. You need to match versions, set paths, and ensure compatibility. cross solves this by using Docker.
cross pulls a pre-built Docker image for your target. That image contains the sysroot, the linker, and the C toolchain. Your source code is mounted into the container. The build runs inside. The binary comes out. You never install the target toolchain on your host machine.
Think of cross as a portable workshop. Instead of trying to build a car engine in your kitchen, you drive to a factory that has the exact wrenches, lifts, and parts for that specific engine. You drop off your design. The factory builds it. You drive away with the finished product. You do not need to own the factory. You just need access to it.
The container is the source of truth. Your local machine is just the trigger.
Getting started
You need Docker installed and running. cross communicates with the Docker daemon to manage containers. Install cross via Cargo.
cargo install cross
# Installs the cross binary. Requires Docker to be running.
Verify the installation.
cross --version
# Confirms cross is available and checks Docker connectivity.
If you see a version number, you are ready. If you see an error about Docker, start the Docker daemon and try again.
Run the command. Check the output. If the binary exists, you are done.
A minimal build
Build a release binary for a Linux musl target. This creates a statically linked executable that runs on almost any Linux distribution.
cross build --target x86_64-unknown-linux-musl --release
# Targets 64-bit Linux with musl libc. Produces a static binary.
The target triple follows the format architecture-vendor-os-environment. x86_64 is the CPU. unknown is the vendor. linux is the OS. musl is the C library environment.
musl is a lightweight C library. Binaries linked against musl do not depend on the host's glibc. This makes distribution easy. You can copy the binary to an Alpine Linux container or a minimal server, and it runs without extra dependencies.
Check the output directory.
file target/x86_64-unknown-linux-musl/release/your-binary
# Shows the binary format and target architecture.
The file command confirms the binary is for the correct architecture. You can now ship this file to any x86_64 Linux machine.
Trust the container. If the image exists, the build works.
What happens under the hood
cross automates a sequence of steps that would otherwise require manual setup. Understanding this sequence helps you debug failures.
crosschecks your Cargo configuration to determine the target.crosspulls the appropriate Docker image. Images are named likerust:cross-x86_64-unknown-linux-musl.crossmounts your project directory and your Cargo registry into the container. This ensures dependency caching works across builds.crosssets environment variables. It configuresCARGO_TARGET_...variables so Cargo knows how to link for the target.crossrunscargo buildinside the container.crosscopies the resulting artifacts back to your host machine.
The environment variables are key. Cargo uses variables like CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER to find the correct linker. cross sets these automatically based on the container's toolchain. You rarely need to touch them.
If a build fails, the error usually comes from step 5. The container runs the same Cargo process you would run locally, but with the target toolchain. If the error is a missing library, the container is missing a package. If the error is a Rust compilation error, your code has a bug.
Read the linker error. It tells you exactly which package is missing.
Real-world configuration
Real projects often have dependencies that require system libraries. A crate using openssl-sys needs the OpenSSL development headers. A crate using sqlite3 needs the SQLite library. cross handles this with a Cross.toml file.
Create Cross.toml in your project root.
# Cross.toml
# Configures cross-compilation settings for the project.
[target.aarch64-unknown-linux-gnu]
# Installs system dependencies before building.
pre-build = [
"apt-get update && apt-get install -y libssl-dev pkg-config",
]
The pre-build key runs commands inside the container before the build starts. This installs the necessary packages. cross caches the container layer, so subsequent builds are fast.
You can also pass environment variables.
[build.env]
# Passes environment variables from the host to the container.
passthrough = ["MY_API_KEY", "CARGO_REGISTRIES_CRATES_IO_PROTOCOL"]
Use passthrough for secrets or configuration that must reach the build script. cross blocks most environment variables by default to ensure reproducibility. Explicitly listing variables makes your build transparent.
Build with the configuration.
cross build --target aarch64-unknown-linux-gnu --release
# Uses Cross.toml to install dependencies and build.
You can also run tests inside the container.
cross test --target aarch64-unknown-linux-gnu
# Runs tests in the container using the target toolchain.
This ensures your tests run against the same libraries as the final binary. It catches issues that only appear on the target platform.
Put your build dependencies in Cross.toml. Never hide them in a script.
Common pitfalls and errors
cross simplifies cross-compilation, but it does not eliminate all problems. Here are the issues you will encounter.
Docker is not running.
cross fails immediately if it cannot connect to the Docker daemon. The error mentions cannot connect to Docker. Start Docker and retry.
Missing system libraries.
If a dependency requires a C library, the build fails with a linker error. The error looks like ld: cannot find -lssl. This means the container is missing the development package. Add the package to pre-build in Cross.toml.
build.rs issues.
Build scripts run during compilation. If your build.rs uses the cc crate, it must respect the target. cross sets the correct environment variables, but some build scripts ignore them. Ensure your build.rs uses cc::Build::new().compile() without hardcoding the compiler path. The cc crate reads the environment variables set by cross.
Non-standard targets.
cross supports standard Rust targets. If you are building for a custom embedded target with a unique toolchain, cross may not have a pre-built image. You can create a custom Docker image and reference it in Cross.toml, but this requires Docker expertise.
Debugging symbols.
cross build produces binaries with debug symbols if you do not use --release. These symbols reference paths inside the container. Debuggers on your host may struggle to resolve them. Use --release for distribution. Use cross run for debugging inside the container.
Performance overhead. Docker adds overhead. The first build pulls the image and installs dependencies. Subsequent builds are faster due to caching, but still slower than a native build. Accept the overhead. It buys you reproducibility and correctness.
Read the logs. The error is almost always a missing system library, not a Rust bug.
Choosing the right tool
Cross-compilation has multiple approaches. Pick the one that matches your needs.
Use cross when you need to compile for a target architecture that isn't available on your host machine. Use cross when you want identical build environments across developer laptops and CI runners. Use cross when your project depends on C libraries that are difficult to install manually. Use cargo build --target when you already have the target linker and sysroot installed and configured on your host. Use zig as a C compiler when you need cross-compilation without the overhead of Docker containers. Use a manual sysroot setup when you are targeting a custom embedded platform that lacks a standard cross image.
Choose the tool that fits the target. Do not over-engineer the build.