Self-hosted service
clux/muslrust avatar
clux/muslrust

muslrust: a Docker image for static Rust binaries

Docker environment for building musl based static rust binaries

1,100 stars92 forksDockerfileMIT

At a glance

What is it?
clux/muslrust is a Docker environment that compiles Rust against musl so the resulting binary is fully static, with the C libraries that Rust crates still need already built into the image. The stable and nightly tags are rebuilt by GitHub Actions, and the C library set is being trimmed over time.
Who is it for?
Adopt muslrust if you want static Rust binaries without maintaining your own musl cross-compilation image, and if your dependencies are covered by the crates its test suite checks. Do not adopt it if your build needs OpenSSL, libcurl, or libpq, since those were removed in 2025.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 127 days ago.
What is it written in?
Mainly Dockerfile, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the image actually does for you

muslrust is a Docker environment for building static Rust binaries for x86_64 and arm64 using musl. The value it offers is not the musl toolchain itself, which any Linux box can install, but the whole toolchain assembled and working inside a container, together with the C libraries that Rust crates still depend on.

The motivation is described in the README as simplifying the creation of small and efficient cloud containers, or stand-alone Linux binary releases. A statically linked binary calls straight into the kernel with no dynamic system library dependencies, which means it can be shipped to most distributions without compatibility issues and dropped as-is into a distroless static, scratch, or Alpine image. The README states the practical outcome plainly: binaries and images for small apps generally end up under 10MB compressed, or around 20MB uncompressed without stripping.

The project is MIT licensed, its primary language is Dockerfile, and there are no GitHub releases at all, because the artefacts here are container images rather than source packages. The distribution channel is Docker Hub, with a stable tag and dated nightly tags, and the last push to the repository was on 2026-06-18. The images are built daily by GitHub Actions, with separate workflows for the nightly and stable channels.

Pull, mount, build

The usage section is two commands, run from the root of a Rust project. You pull the stable image and then run a container with the project directory mounted:

sh
docker pull clux/muslrust:stable
docker run -v $PWD:/volume --rm -t clux/muslrust:stable cargo build --release

The container writes its output back to the mounted volume, so the binary lands in the host's target directory. The result is a musl-target executable, and the README shows how you check it, with the expected output stating that the file is not a dynamic executable:

sh
ldd target/x86_64-unknown-linux-musl/release/EXECUTABLE
        not a dynamic executable

That check is worth keeping in your build script. A build that silently produces a glibc dynamic binary still works on your build machine and then fails on a distroless image at runtime, so the ldd assertion is the cheapest way to catch that.

The tags are worth understanding too. The standard tags are :stable or a dated :nightly-{YYYY-mm-dd}, and for pinned or historical builds there is a list of available tags on Docker Hub. Using a dated tag is how you make a container build reproducible, since the stable tag moves whenever the image is rebuilt.

The C library set is shrinking on purpose

The most interesting part of the project is the list of C libraries compiled against musl-gcc inside the image. Currently there are two. sqlite3 is provided through the libsqlite3-sys crate used by diesel, and zlib is also included. Both are updated with renovate from git releases.

The README is explicit that these dependencies may be removed if sensible and popular Rust crates can replace them in the future, and it keeps a list of what has already been removed. openssl was removed in 2025, with the issue reference #153. curl was removed in 2025, reference #96. pq was removed in 2025, reference #81. The direction of travel is that fewer C libraries ship in the image over time, because each one is a maintenance liability and a licence to track.

If you need the removed libraries, the README points to blackdex/rust-musl as an alternative for openssl, curl, and pq, and it also says you can temporarily add them back to your own image following a specific comment on issue #168. That last route is the escape hatch for anyone whose crate still links libpq, and it keeps the shared image clean for everyone else.

The Rust ecosystem context is what makes the removals possible. A crate that needs TLS can use rustls instead of openssl, and a crate that needs an HTTP client can use one built on rustls. The image is following the ecosystem rather than leading it, and the removals are the record of that.

The test set is the compatibility contract

Before a new version of muslrust is pushed, the project tests that certain crates can be used and statically linked. The list is short and specific: serde, diesel using sqlite, rustls, hyper using hyper-rustls and rustls's default crypto backend, flate2, and rand.

This list is more informative than a general compatibility claim, because each entry names a category of build problem. serde exercises the derive machinery and generics, diesel and sqlite cover a C library that must be compiled against musl-gcc, rustls covers a pure Rust TLS stack, hyper covers an async server with TLS, and flate2 covers compression. If you are working on a crate that links something not on this list, you are in uncharted territory and the test set is the place to look first.

The build and test machinery is driven by a justfile rather than a Makefile, which is the modern Rust convention. The targets are explicit about platform and channel combinations:

code
build-stable-amd: (_build "stable" "linux/amd64")
build-nightly-amd: (_build "nightly" "linux/amd64")
build-stable-arm: (_build "stable" "linux/arm64")
build-nightly-arm: (_build "nightly" "linux/arm64")

The test targets follow, one per crate, with a local variant that derives the platform from the host and a CI variant that inherits the platform from environment variables. The justfile also has a run target that mounts a test directory into the built container and a test-setup target that builds a separate test runner image from Dockerfile.test-runner.

Rebuilds are slow without a cargo cache volume

The caching section addresses a problem everyone hits on the first attempt. Repeat builds locally are always from scratch, and therefore slow, without a cached cargo directory, because every container starts with an empty cargo registry. The fix is to add a Docker volume mounted at the cargo registry path:

sh
musl-build() {
  docker run \
    -v cargo-cache:/root/.cargo/registry \
    -v "$PWD:/volume" \
    --rm -it clux/muslrust cargo build --release
}

The README suggests adding that function to your shell profile. The volume persists between runs, so dependencies are downloaded once, and it can be inspected with docker volume inspect cargo-cache. Without it, every rebuild re-downloads the crate index and all dependencies, which on a cold cache is longer than the compile itself.

CI needs the same consideration, and the README lists both approaches: store the cargo-cache folder, or rely on Docker layer caches with layers, for which it points at cargo-chef. GitHub Actions supports both methods, with a direct folder cache example and a Docker layer cache example using the builder pattern, and CircleCI supports both as well. The distinction matters because the two approaches have different failure modes, and layer caching depends on the ordering of your Dockerfile instructions.

Base image choice, allocator tuning, and the bits that bite

The Dockerfile starts from ubuntu:resolute and maps the Docker target architecture to the uname equivalent, setting DOCKER_TARGET_ARCH to x86_64 for the amd64 stage and aarch64 for the arm64 stage, because the toolchain in the image expects one spelling and Docker uses the other. It installs musl-dev and musl-tools as the musl toolchain, and the build tools needed for third party libraries, plus ca-certificates for download verification, git for cargo builds, and automake, autoconf, and libtool for crates that build C dependencies as part of a cargo build. It also fetches a pinned protoc release, version v34.2, into /usr/local/bin.

For memory behaviour, the README points to changing the global allocator in sensitive applications, with an example using the jemallocator crate:

rust
use tikv_jemallocator::Jemalloc;
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;

Mimalloc is offered as the other option. This is a general performance note rather than a muslrust feature, and it is filed under the project because musl's allocator behaviour is a common source of surprise when moving an application from glibc.

One more practical trap is documented under troubleshooting, namely filesystem permissions on local builds, where the permissions of the musl parts of the target artifacts directory end up owned by root because the container runs as root while the files are written to the host volume. It is the first thing to look for when a local build leaves unreadable artefacts.

Against your own cross-compilation image

The alternative is to build your own toolchain container, or to cross compile from the host with a musl target and a musl-capable linker. The difference in approach is ownership. Rolling your own gives you exact control over the base image, the pinned toolchain version, and the C libraries, and it is the right answer when you have a specific reason to pin things muslrust does not, for example a particular openssl version or a custom protoc.

The cost of rolling your own is that someone maintains it, and the failure mode is quiet. A container that is missing a C library does not fail loudly at build time in an obvious way, it fails at link time with a confusing error, or worse, it produces a binary that links dynamically and then fails in production. muslrust's test set exists to prevent exactly that, and its daily rebuilds mean the base image does not rot while your project is busy.

The third option, writing your own Docker multi-stage build for each project, is the one the README's example links point at. A Kubernetes controller using actix-web with plain distroless static, a reflector with axum using the builder pattern, a controller using cargo-chef for layer caching, and release assets uploaded through GitHub Actions. These are the shapes you would end up writing anyway once you have a working muslrust image, since the final image is a distroless static or chainguard static base with your binary copied in, and the build cache behaviour is a property of your own Dockerfile.

For the production image itself, the README is opinionated: distroless static or chainguard static are the recommended bases, because they contain a non-root user and SSL certificates, unlike scratch, and they disallow shell access.

Editorial conclusion

Adopt muslrust if you want static Rust binaries without maintaining your own musl cross-compilation image, and if your dependencies are covered by the crates its test suite checks. Do not adopt it if your build needs OpenSSL, libcurl, or libpq, since those were removed in 2025. Verify first that the C libraries your crate uses are among sqlite3 and zlib, and add a cargo registry volume before your first build if the download time matters.

Frequently asked questions

How do I build a static Rust binary with muslrust?

Pull the image and run it with the project directory mounted, using docker pull clux/muslrust:stable followed by docker run -v $PWD:/volume --rm -t clux/muslrust:stable cargo build --release. The result lands in the target directory as an x86_64-unknown-linux-musl executable.

How do I check that the produced binary is really static?

Run ldd on the executable. The README shows the expected output as not a dynamic executable, which is the check worth putting in a build script, since a build that silently produces a dynamic binary fails later on a distroless image.

Which C libraries does the muslrust image include?

Two are compiled against musl-gcc and included: sqlite3, used through the libsqlite3-sys crate by diesel, and zlib. Both are updated with renovate from git releases. openssl, curl, and pq were removed in 2025.

Why are my local muslrust rebuilds so slow?

Repeat local builds are always from scratch without a cached cargo directory, so every container re-downloads dependencies. The README suggests adding a Docker volume with -v cargo-cache:/root/.cargo/registry, which you can then reuse through a shell function.

Which crates does muslrust test for static linking before a release?

The test set covers serde, diesel using sqlite, rustls, hyper using hyper-rustls, flate2, and rand. A crate linking something outside that set, such as libpq, is not covered by the project test matrix.

Official sources

  1. clux/muslrust on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/clux-muslrust.svg)](https://hysenlabs.com/projects/clux-muslrust)

Was this useful?