docs/sandboxes/overview.mdx
A sandbox is a microVM: a real virtual machine with its own Linux kernel, filesystem, and network stack, running as a child process of whatever application creates it.
The security boundary here is hardware virtualization, not Linux namespaces. Container escapes are a well-documented class of vulnerability; breaking out of a microVM requires exploiting the hypervisor itself, which is a fundamentally harder problem.
At minimum, a sandbox needs a name and an image. Everything else has sensible defaults: 1 vCPU, 512 MiB memory, public-only networking, /bin/sh as the shell.
await using sb = await Sandbox.builder("worker")
.image("python")
.create();
sb = await Sandbox.create("worker", image="python")
msb create python --name worker
import { Sandbox } from "microsandbox";
await using sb = await Sandbox.builder("worker")
.image("python")
.memory(1024)
.cpus(2)
.env("DEBUG", "true")
.env("API_PORT", "8000")
.volume("/app/src", (m) => m.bind("./src").readonly())
.volume("/data", (m) => m.named("my-data"))
.volume("/tmp/scratch", (m) => m.tmpfs().size(100))
.create();
from microsandbox import Sandbox, Volume
sb = await Sandbox.create(
"worker",
image="python",
memory=1024,
cpus=2,
env={"DEBUG": "true", "API_PORT": "8000"},
volumes={
"/app/src": Volume.bind("./src", readonly=True),
"/data": Volume.named("my-data"),
"/tmp/scratch": Volume.tmpfs(size_mib=100),
},
)
msb create python --name worker \
-c 2 \
-m 1G \
-e DEBUG=true \
-e API_PORT=8000 \
-v ./src:/app/src:ro \
-v my-data:/data \
--tmpfs /tmp/scratch:100
| Option | Default | Description |
|---|---|---|
image | — | OCI image, local path, or disk image |
cpus | 1 | Number of virtual CPUs |
memory | 512 | Guest memory in MiB |
workdir | — | Default working directory for commands |
shell | /bin/sh | Shell used by shell() calls |
env | {} | Environment variables |
volumes | [] | Volume mounts (bind, named, or tmpfs) |
network | public | Network policy and port mappings |
scripts | {} | Named scripts stored at /.msb/scripts/ |
microsandbox supports three ways to provide a root filesystem. The choice affects how the filesystem is assembled and what features are available.
The most common option. microsandbox pulls the image and stacks its layers as a copy-on-write filesystem. Changes inside the sandbox don't modify the base image. If two sandboxes use the same image, they share the same cached layers on disk.
<CodeGroup> ```rust Rust Sandbox::builder("worker").image("python").create().await?; ```await Sandbox.builder("worker").image("python").create();
await Sandbox.create("worker", image="python")
msb create python --name worker
Use a local directory on the host as the root filesystem directly. The guest sees the directory contents as its /. This is useful for development when you have a pre-built rootfs, or for minimal environments where you've assembled the filesystem yourself.
await Sandbox.builder("worker").image("./my-rootfs").create();
await Sandbox.create("worker", image="./my-rootfs")
msb create ./my-rootfs --name worker
Boot from a QCOW2, Raw, or VMDK disk image. Unlike OCI images (which use a copy-on-write overlay), disk images give the guest raw block device access. See Disk Images for details.
<CodeGroup> ```rust Rust // Auto-detect filesystem Sandbox::builder("worker") .image("./ubuntu-22.04.qcow2") .create() .await?;// Explicit filesystem type Sandbox::builder("worker") .image_with(|i| i.disk("./alpine.raw").fstype("ext4")) .create() .await?;
```typescript TypeScript
// Auto-detect the disk image format from the file extension.
await Sandbox.builder("worker")
.image("./alpine.qcow2")
.create();
from microsandbox import Image, Sandbox
# Auto-detect filesystem
await Sandbox.create("worker", image="./ubuntu-22.04.qcow2")
# Explicit filesystem type
await Sandbox.create("worker", image=Image.disk("./alpine.raw", fstype="ext4"))
msb create ./alpine.qcow2 --name worker
Replace a stopped or running 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.
<CodeGroup> ```rust Rust let sb = Sandbox::builder("worker") .image("python") .replace() .create() .await?; ```await using sb = await Sandbox.builder("worker")
.image("python")
.replace()
.create();
sb = await Sandbox.create("worker", image="python", replace=True)
msb create --replace python --name worker
If the existing sandbox is still running, microsandbox sends it SIGTERM and waits for it to exit before falling back to SIGKILL. The default wait is 10 seconds. Pass a longer grace if the workload needs more time to shut down cleanly, or 0 to skip SIGTERM and kill immediately.
let sb = Sandbox::builder("worker") .image("python") .replace_with_grace(Duration::from_secs(30)) .create() .await?;
```typescript TypeScript
await using sb = await Sandbox.builder("worker")
.image("python")
.replaceWithGrace(30_000) // milliseconds
.create();
sb = await Sandbox.create("worker", image="python", replace_with_grace=30) # seconds
msb create --replace-with-grace 30s python --name worker
msb create --replace-with-grace 0 python --name worker # SIGKILL immediately
replace_with_grace / --replace-with-grace implies replace / --replace, so you don't need to pass both.
Authenticate to private container registries and control when images are pulled.
<CodeGroup> ```rust Rust use microsandbox::{Sandbox, RegistryAuth, PullPolicy};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?;
```typescript TypeScript
import { Sandbox } from "microsandbox";
await using sb = await Sandbox.builder("worker")
.image("private.registry.io/org/app:v1")
.registry((r) => r.auth({
kind: "basic",
username: "deploy",
password: process.env.REGISTRY_TOKEN!,
}))
.pullPolicy("always")
.create();
import os
from microsandbox import PullPolicy, RegistryAuth, Sandbox
sb = await Sandbox.create(
"worker",
image="private.registry.io/org/app:v1",
registry_auth=RegistryAuth.basic("deploy", os.environ["REGISTRY_PASSWORD"]),
pull_policy=PullPolicy.ALWAYS,
)
msb registry login private.registry.io -u deploy
msb create private.registry.io/org/app:v1 \
--name worker --pull always
| Policy | Behavior |
|---|---|
"always" | Pull the image every time, even if cached locally |
"if-missing" | Pull only if the image is not already cached (default) |
"never" | Never pull; fail if the image is not cached |
Build a sandbox configuration without booting the VM. Useful for serializing, storing, validating, or modifying configs before creation.
<CodeGroup> ```rust Rust let config = Sandbox::builder("preview") .image("python") .memory(1024) .build()?;println!("{}", serde_json::to_string_pretty(&config)?); let sb = Sandbox::create(config).await?;
```typescript TypeScript
const builder = Sandbox.builder("preview").image("python").memory(1024);
const config = builder.build();
console.log(JSON.stringify(config, null, 2));
const sb = await builder.create();
import json
from microsandbox import Sandbox
config = Sandbox.build_config(
"preview",
image="python",
memory=1024,
)
print(json.dumps(config, indent=2))
sb = await Sandbox.create(config)
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:
/.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.