Back to Microsandbox

Overview

docs/sandboxes/overview.mdx

0.4.59.9 KB
Original Source

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.

Creating a sandbox

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.

<CodeGroup> ```rust Rust let sb = Sandbox::builder("worker") .image("python") .create() .await?; ```
typescript
await using sb = await Sandbox.builder("worker")
    .image("python")
    .create();
python
sb = await Sandbox.create("worker", image="python")
bash
msb create python --name worker
</CodeGroup>

Configuration options

<CodeGroup> ```rust Rust let sb = Sandbox::builder("worker") .image("python") .memory(1024) .cpus(2) .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)) .create() .await?; ```
typescript
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();
python
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),
    },
)
bash
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
</CodeGroup>
OptionDefaultDescription
imageOCI image, local path, or disk image
cpus1Number of virtual CPUs
memory512Guest memory in MiB
workdirDefault working directory for commands
shell/bin/shShell used by shell() calls
env{}Environment variables
volumes[]Volume mounts (bind, named, or tmpfs)
networkpublicNetwork policy and port mappings
scripts{}Named scripts stored at /.msb/scripts/
<Tip> `cpus` and `memory` are **limits, not reservations**. Setting `memory: 512` doesn't allocate 512 MiB upfront. Physical pages are only allocated as the guest actually touches them, so you can comfortably run many sandboxes on a single host without worrying about overcommitting. </Tip>

Rootfs sources

microsandbox supports three ways to provide a root filesystem. The choice affects how the filesystem is assembled and what features are available.

OCI images

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?; ```
typescript
await Sandbox.builder("worker").image("python").create();
python
await Sandbox.create("worker", image="python")
bash
msb create python --name worker
</CodeGroup>

Bind mounts

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.

<CodeGroup> ```rust Rust Sandbox::builder("worker").image("./my-rootfs").create().await?; ```
typescript
await Sandbox.builder("worker").image("./my-rootfs").create();
python
await Sandbox.create("worker", image="./my-rootfs")
bash
msb create ./my-rootfs --name worker
</CodeGroup> <Note> The guest agent is automatically included in the rootfs during sandbox creation, regardless of rootfs source. You don't need to add anything to your image or directory. </Note>

Disk images

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();
python
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"))
bash
msb create ./alpine.qcow2 --name worker
</CodeGroup> <Note> 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). </Note>

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.

<CodeGroup> ```rust Rust let sb = Sandbox::builder("worker") .image("python") .replace() .create() .await?; ```
typescript
await using sb = await Sandbox.builder("worker")
    .image("python")
    .replace()
    .create();
python
sb = await Sandbox.create("worker", image="python", replace=True)
bash
msb create --replace python --name worker
</CodeGroup>

Private registry

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();
python
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,
)
bash
msb registry login private.registry.io -u deploy
msb create private.registry.io/org/app:v1 \
  --name worker --pull always
</CodeGroup>
PolicyBehavior
"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
<Tip> Once an OCI image is resolved, microsandbox pins the exact layers. A `python` 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. </Tip>

Config inspection <sup><sup>coming soon</sup></sup>

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();
python
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)
</CodeGroup>

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.