Create with options
The builder/options pattern lets you configure everything about a sandbox before booting it.
use microsandbox::{Sandbox, NetworkPolicy};
let sb = Sandbox::builder("worker")
.image("python:3.12")
.memory(1024)
.cpus(2)
.workdir("/app")
.shell("/bin/bash")
.env("DEBUG", "true")
.env("API_PORT", "8000")
.volume("/app/src", |v| v.bind("./src").readonly())
.volume("/data", |v| v.named("my-data"))
.volume("/tmp/scratch", |v| v.tmpfs().size(100))
.network(|n| n.policy(NetworkPolicy::public_only()))
.create()
.await?;
Detached creation
By default, a sandbox is “attached” to your process: when your process exits, the sandbox is stopped and cleaned up. Detached sandboxes survive independently and you can reconnect to them later.
let sb = Sandbox::builder("worker")
.image("python:3.12")
.create_detached()
.await?;
Replace existing
Replace a stopped sandbox with the same name instead of failing on conflict. This stops the old sandbox (if still running), removes it, and creates a fresh one.
let sb = Sandbox::builder("worker")
.image("python:3.12")
.replace()
.create()
.await?;
Config inspection
Build a sandbox configuration without booting the VM. Useful for serializing, storing, validating, or modifying configs before creation.
use microsandbox::Sandbox;
let config = Sandbox::builder("preview")
.image("python:3.12")
.memory(1024)
.build()?;
println!("{}", serde_json::to_string_pretty(&config)?);
let sb = Sandbox::create(config).await?;
Disk image rootfs
Boot from a QCOW2, Raw, or VMDK disk image instead of an OCI container image. The guest gets raw block device access. The format is auto-detected from the file extension, or you can specify it explicitly along with the filesystem type.
use microsandbox::Sandbox;
// Auto-detect filesystem
let sb = Sandbox::builder("vm-sandbox")
.image("./ubuntu-22.04.qcow2")
.cpus(2)
.memory(2048)
.create()
.await?;
// Explicit filesystem type
let sb = Sandbox::builder("vm-sandbox")
.image(|i| i.disk("./alpine.raw").fstype("ext4"))
.cpus(1)
.memory(512)
.create()
.await?;
With OCI images, microsandbox stacks layers and adds a copy-on-write overlay so sandboxes share the base. With disk images, the guest gets the block device directly, so there’s no copy-on-write isolation between sandboxes using the same disk image. Each sandbox needs its own copy (or use QCOW2’s built-in snapshot/backing file support).
Private registry
Authenticate to private container registries and control when images are pulled.
use microsandbox::{Sandbox, RegistryAuth, PullPolicy};
// Explicit auth with pull policy
let sb = Sandbox::builder("worker")
.image("registry.corp.io/team/app:latest")
.registry_auth(RegistryAuth::Basic {
username: "deploy".into(),
password: std::env::var("REGISTRY_PASSWORD")?,
})
.pull_policy(PullPolicy::Always)
.create()
.await?;
// Use cached image, never pull
let sb = Sandbox::builder("offline")
.image("python:3.12")
.pull_policy(PullPolicy::Never)
.create()
.await?;
Pull policies control whether the SDK fetches the image from the registry:
| Policy | Behavior |
|---|
Always | Pull the image every time, even if cached locally |
IfMissing | Pull only if the image is not already cached (default) |
Never | Never pull; fail if the image is not cached |
Once an OCI image is resolved, microsandbox pins the exact layers. A python:3.12 reference is resolved to an immutable set of layers at first pull. Subsequent start() calls use the pinned layers without re-resolving the mutable tag, so your sandbox is reproducible even if the upstream tag is updated.
Lifecycle
Stop, start, kill
Stop gracefully terminates guest processes. Start reboots a stopped sandbox from persisted state. Kill force-terminates when the guest is unresponsive.
use microsandbox::Sandbox;
sb.stop().await?;
let sb = Sandbox::start("worker").await?;
sb.kill().await?;
Pause and resume
Freeze all guest processes. Zero CPU usage while paused. Resume is instant with no re-boot.
sb.pause().await?;
sb.resume().await?;
Drain
Graceful shutdown that lets existing commands finish but rejects new ones. Transitions to Stopped when all in-flight commands complete.
Detach
Release the sandbox handle without stopping it. The sandbox continues running as a background process.
Wait
Block until the sandbox exits on its own, without triggering a stop.
let exit_status = sb.wait().await?;
List, get, remove
use microsandbox::Sandbox;
for handle in Sandbox::list().await? {
println!("{}: {:?}", handle.name(), handle.status());
}
let handle = Sandbox::get("worker").await?;
let sb = handle.start().await?;
Sandbox::remove("old-sandbox").await?;
Graceful shutdown with timeout fallback
Attempt a graceful stop, then force-kill if the sandbox doesn’t shut down in time.
use microsandbox::Sandbox;
use std::time::Duration;
let mut handle = Sandbox::get("worker").await?;
match tokio::time::timeout(Duration::from_secs(30), handle.stop()).await {
Ok(Ok(())) => println!("Stopped"),
Ok(Err(e)) => eprintln!("Error: {e}"),
Err(_) => handle.kill().await?,
}
Sandbox Process Policies
Configure how the sandbox handles shutdown escalation, idle detection, and maximum lifetime. These policies are enforced on the host side, so the guest can’t override them.
use microsandbox::{Sandbox, LogLevel};
let sb = Sandbox::builder("worker")
.image("python:3.12")
.max_duration(3600)
.idle_timeout(300)
.log_level(LogLevel::Debug)
.create()
.await?;
| Option | Default | Description |
|---|
max_duration | none | Maximum sandbox lifetime in seconds; triggers drain when exceeded |
idle_timeout | none | Auto-drain after this many seconds of inactivity |
log_level | none | Override sandbox-process log verbosity |
Customizing the guest environment
If you need to run setup logic, install packages, or inject files before a sandbox starts doing real work, there are two ways to do it without building a custom image:
- Scripts are files mounted into the sandbox at
/.msb/scripts/ and added to PATH. Define them at creation time and call them by name with exec() or shell(). Good for multi-step setup procedures or named entry points.
- Patches modify the rootfs before the VM boots: write config files, copy directories from the host, create symlinks, append to existing files, etc. The base image stays untouched since patches go into the writable layer.