rightsize-rust
Testcontainers-style integration testing on microVMs. No Docker required.
rightsize-rust runs your integration-test containers as hardware-isolated microsandbox microVMs — one microVM per container — behind a Tokio-async-native, RAII-guard Rust API. The runtime self-provisions on first use (one Cargo dependency, zero install steps), and a hand-rolled Docker backend covers the platforms microVMs can't reach.
use RedisContainer;
async
The guard is the API: start() returns an RAII handle. Call stop().await for
explicit, ordered teardown, or just drop it — a dedicated cleanup thread reclaims
the container even if the test panics (how it works).
Why microVMs
| Docker + Testcontainers | rightsize-rust | |
|---|---|---|
| Isolation | shared kernel (containers) | hardware-level (microVM per container) |
| Runtime install | Docker Desktop / daemon required | none — self-provisions on first use |
| Licensing | Docker Desktop licensing in orgs | Apache-2.0 all the way down |
| Async model | blocking client calls | async fn throughout — no thread-per-container blocking |
| Docker client | bollard/shiplift over hyper |
hand-rolled, unix-socket-only — can't be misrouted onto TCP |
The Docker client's transport is hand-rolled by design: a small layer over
tokio::net::UnixStream, so a dependency bump elsewhere in your tree can never be
the reason a Docker call gets misrouted onto TCP — no bollard, no hyper, ever. The
JSON layer on top of it is ordinary serde/serde_json, which carries none of that
transport-routing risk.
Quickstart
# Cargo.toml
[]
= "0.1.0"
= "0.1.0"
= { = "1", = ["macros", "rt-multi-thread"] }
rightsize-modules pulls in both backend crates by default (the backend-msb
and backend-docker Cargo features); disable default-features and re-enable
one to trim the dependency you don't need.
Rust has no ServiceLoader-style plugin discovery, so register the backend(s)
once per process before the first start():
use Once;
static REGISTER: Once = new;
Call it at the top of a shared test-fixture module (or your binary's startup
path). On first test run, rightsize-rust downloads the pinned microsandbox runtime
(SHA-256-verified, from GitHub releases) into ~/.cache/rightsize/ and boots your
containers as microVMs — rightsize::backends::resolve picks among whatever's
registered, honoring RIGHTSIZE_BACKEND when set. No daemon, no root, no
pre-installed anything.
Driving a container by hand
Skip the module helpers and use Container directly for any image:
use ;
let arango = new
.with_env
.with_exposed_ports
.waiting_for
.start
.await?;
let port = arango.get_mapped_port?; // published on 127.0.0.1
// ... your test ...
arango.stop.await?;
Sharing one container across tests
rightsize-rust has no JUnit-style static-scope annotation — the RAII guard is the
whole API surface. To share a container across many tests in one binary, boot it
once behind a tokio::sync::OnceCell and let process exit reclaim it:
use Arc;
use OnceCell;
use ;
static REDIS: = const_new;
async
async
Backends
rightsize-rust picks a backend automatically; override with
RIGHTSIZE_BACKEND=microsandbox|docker.
| Platform | Backend used |
|---|---|
| macOS (Apple Silicon) | microsandbox (microVMs) |
Linux x86_64 / arm64 with /dev/kvm |
microsandbox (microVMs) |
| Windows x86_64 / arm64 with WHP enabled | microsandbox (microVMs)¹ |
| Intel Mac | Docker (auto-fallback) |
| Windows without WHP | Docker (auto-fallback) |
| Linux without KVM | Docker (auto-fallback) |
¹ Windows support runs on the Windows Hypervisor Platform (WHP) and is upstream
beta. CI-verified on windows-2022/windows-2025 hosted runners, where WHP is
already enabled with no reboot required. If WHP isn't enabled on your machine,
RIGHTSIZE_BACKEND=microsandbox fails naming the precondition (run
msb doctor --fix in an elevated terminal — this may require a reboot); leaving
RIGHTSIZE_BACKEND unset falls back to Docker silently.
Both backends satisfy one behavioral contract (SandboxBackend), verified by a
shared test suite — the tests you write run unchanged on either. A few edges are
backend-specific rather than behavioral divergences:
- Network-alias tunnels on microsandbox have real limits versus Docker's native bridge networking — see Networking.
- Read-only file mounts aren't enforced in-guest on microsandbox 0.6.2.
FileMount::read_onlyis honored by Docker; on microsandbox the guest currently gets a writable mount regardless. Don't rely on guest-side write protection underRIGHTSIZE_BACKEND=microsandbox. follow_outputdelivers the same ordered, no-duplicate log stream on both backends, but on microsandbox the final tail can arrive shortly after the sandbox reports stopped, rather than exactly at stream EOF (msb logs -fdoesn't close on sandbox stop in 0.6.2, so the backend replays the not-yet-delivered tail once stop is confirmed).
Modules
rightsize-modules ships eighteen preconfigured containers with sensible waits
and connection helpers. Each is a thin newtype wrapping Container, so its guard
exposes typed accessors while the core builders (with_env, waiting_for, …)
remain available.
| Module | Helpers |
|---|---|
RedisContainer |
uri() |
MemcachedContainer |
address() — protocol-level VERSION probe, not a bare port wait |
ArangoContainer |
endpoint(); with_root_password(…) to enable auth (default: no-auth) |
MongoDbContainer |
connection_string() — single-node replica set, auto-initiated |
PostgresContainer |
connection_string(), username(), password(), database_name(); with_username/with_password/with_database(…) |
MySqlContainer |
connection_string(), username(), password(), database_name(); with_username/with_password/with_database(…) |
MariaDbContainer |
connection_string(), username(), password(), database_name(); with_username/with_password/with_database(…) |
ClickHouseContainer |
http_url(), username(), password(), database_name(); with_username/with_password/with_database(…) |
Neo4jContainer |
http_url(), bolt_url(), username(), password(); with_password(…) — HTTP Cypher endpoint (username fixed at neo4j) |
RedpandaContainer |
bootstrap_servers(), schema_registry_url() |
KafkaContainer |
bootstrap_servers() — KRaft single node |
RabbitMqContainer |
amqp_url(), management_url(), username(), password(); with_username/with_password(…) |
PinotContainer |
controller_url(), broker_url() — single-container QuickStart cluster |
WireMockContainer |
base_url(), admin_url() — stub via the /__admin API |
KeycloakContainer |
auth_server_url(), management_url(), admin_username(), admin_password(); with_admin_username/with_admin_password(…) |
SpringCloudConfigContainer |
uri() |
FlociContainer |
endpoint_url(); FlociContainer::aws()/azure()/gcp() factories — floci.io cloud emulators (unsigned REST, no SDK needed) |
FlinkContainer |
rest_url(); with_task_manager() for a full session cluster — Docker only¹ |
Heavyweight JVM images raise their own memory floor via with_memory_limit —
SpringCloudConfig, Keycloak, Neo4j and Flink (1024 MB), Pinot's four-JVM cluster
(4096 MB). That's baked into the module; you don't set it. Each module's rustdoc
documents its exact image tag, wait strategy, and the reasoning behind those
choices — the module chapter of the book collects
the worked examples.
¹ with_task_manager() returns a Result: on microsandbox it errs with
RightsizeError::UnsupportedByBackend (the Flink image carries no nc/busybox
for the network-link emulation — see Networking), naming the
docker backend as the remedy. A bare JobManager (rest_url() only) runs on both.
Networking
Network gives containers alias-based connectivity on both backends:
use ;
use Arc;
let net = new;
let config = new
.with_network
.with_network_aliases
.with_exposed_ports
.start
.await?;
let app = new
.with_network
.with_env
.start
.await?;
Network::resolve(alias, port) returns alias:port on both backends. On
Docker that's a native network alias. On microsandbox — where microVMs are fully
isolated from each other — rightsize-rust transparently installs an /etc/hosts
entry plus a TCP relay tunneled over the sandbox's exec channel.
The microVM emulation has limits worth knowing: start dependencies before their
consumers, one connection at a time per tunnel (fine for config fetches; not for
a cross-container Kafka consumer), and the consumer image needs nc/busybox.
Violations fail fast with an actionable error.
How it works
- Self-provisioning runtime. A pinned
msbrelease (binary + libkrunfw) is downloaded once, SHA-256-verified against the release manifest, and installed atomically under~/.cache/rightsize/— the binary lands last, so a crashed install is detected and repaired, never half-trusted. A cross-process lock keeps parallel test binaries from racing. - Attached-mode supervision. Each container is a held child process
supervising its microVM; the image's
ENTRYPOINTruns exactly as it would under Docker. - Pre-allocated ports. Host ports are chosen before boot, so brokers like Redpanda/Kafka get their advertised listeners baked in — no restart dance. A backend binds the ports it's given; it never allocates its own.
- One trait, two runtimes.
SandboxBackendis a smallasync_trait; the shared contract suite is the referee, with the Docker backend doubling as the correctness oracle for the microVM backend. - Two-tier cleanup, no async
Drop. The happy path is an explicit, awaitedguard.stop().await. The fallback — a guard simply dropped, a test panicking mid-body — hands a small teardown descriptor to a dedicated background OS thread that tears the container down with blocking I/O only, no Tokio required. A run-id-scoped orphan reaper at backend startup is the backstop for the backstop, cleaning up whatever a prior process's hardSIGKILLleft behind.
Configuration
| Env var | Effect |
|---|---|
RIGHTSIZE_BACKEND |
Force microsandbox or docker |
MSB_PATH |
Use a pre-installed msb binary; skip downloads |
RIGHTSIZE_CACHE_DIR |
Relocate the runtime cache (default ~/.cache/rightsize, %LOCALAPPDATA%\rightsize on Windows) |
RIGHTSIZE_MSB_SKIP_DOWNLOAD |
true = fail instead of downloading (air-gapped CI) |
Examples
Runnable, minimally-commented examples live under
crates/rightsize-modules/examples/, covering
the plain API, a real client round-trip, and multi-container networking:
Like everything else in this repo, they run on either backend — force one with
RIGHTSIZE_BACKEND=microsandbox|docker prefixed on any of the commands above.
Development
MSRV is 1.85 (2024 edition), pinned in rust-toolchain.toml.
# Integration tests boot real containers; they live behind the `sandbox-it`
# feature (not #[ignore]), and you force a backend to pick a lane:
RIGHTSIZE_BACKEND=microsandbox RIGHTSIZE_BACKEND=docker
CI runs the full matrix (unit, msb-linux, msb-macos, docker-fallback).
A change to a SandboxBackend implementation, or anything the shared contract
suite exercises, should run the sandbox-it suite against both backends before a
PR — see CONTRIBUTING.md.
Documentation
The book (source under docs/, built with mdbook build from the repo
root) covers getting started, core concepts (containers & guards, wait strategies,
networking, files & resources), backends, every module, and the internals — all its
samples are machine-compile-verified.