Skip to main content

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:
PolicyBehavior
AlwaysPull the image every time, even if cached locally
IfMissingPull only if the image is not already cached (default)
NeverNever 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.
sb.drain().await?;

Detach

Release the sandbox handle without stopping it. The sandbox continues running as a background process.
sb.detach().await;

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?;
OptionDefaultDescription
max_durationnoneMaximum sandbox lifetime in seconds; triggers drain when exceeded
idle_timeoutnoneAuto-drain after this many seconds of inactivity
log_levelnoneOverride 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.