docs/sandboxes/volumes.mdx
Volumes give a sandbox direct filesystem access to host-side storage or in-memory filesystems. They're useful for persisting data across restarts, sharing data between sandboxes, or mounting host data into the guest. They're also significantly faster than the filesystem API (which transfers files individually), so for anything beyond ad-hoc reads and writes, volumes are the way to go.
microsandbox supports four types of mounts: bind mounts, named volumes, tmpfs, and disk-image volumes.
Mounts use normal Linux behavior by default: writable, executable, suid-enabled, and device-enabled. Add readonly / ro when the guest should not write, noexec when files on the mount should not be executed directly, nosuid when setuid/setgid bits should be ignored, and nodev when device files should be ignored. noexec does not stop interpreters from reading scripts from the mount, for example sh /app/src/script.sh.
For stronger in-guest hardening, create the sandbox with the restricted security profile. Restricted sandboxes set no_new_privs, drop mount-admin capability from user commands, and force nosuid,nodev on user mounts. This profile is incompatible with workloads such as sudo and Docker-in-Docker.
CLI mount flags use SOURCE:DEST[:OPTIONS]. The : before OPTIONS starts the option block, and commas separate options inside that block.
| Flag | Source | Options |
|---|---|---|
--mount-dir | Host directory | ro, rw, noexec, nosuid, nodev, stat-virt=strict|relaxed|off, host-perms=private|mirror |
--mount-file | Host file | ro, rw, noexec, nosuid, nodev, stat-virt=strict|relaxed|off, host-perms=private|mirror |
--mount-named | Named volume | ro, rw, noexec, nosuid, nodev, kind=dir|disk, size=<size> for kind=disk, quota=<size> for directory volumes; directory-backed named volumes also support stat-virt=strict|relaxed|off, host-perms=private|mirror |
--mount-disk | Host disk image | ro, rw, noexec, nosuid, nodev, format=raw|qcow2|vmdk, fstype=<type> |
--tmpfs | Guest path | ro, rw, noexec, nosuid, nodev; size may be passed as PATH:SIZE[:OPTIONS] |
Mount a directory from the host directly into the sandbox. Changes inside the sandbox are reflected on the host, and vice versa.
<CodeGroup> ```rust Rust let sb = Sandbox::builder("dev") .image("python") .volume("/app/src", |v| v.bind("./src").readonly().noexec()) .volume("/app/data", |v| v.bind("./data")) .create() .await?; ```import { Sandbox } from "microsandbox";
await using sb = await Sandbox.builder("dev")
.image("python")
.volume("/app/src", (m) => m.bind("./src").readonly().noexec())
.volume("/app/data", (m) => m.bind("./data"))
.create();
from microsandbox import Sandbox, Volume
sb = await Sandbox.create(
"dev",
image="python",
volumes={
"/app/src": Volume.bind("./src", readonly=True, noexec=True),
"/app/data": Volume.bind("./data"),
},
)
sb, err := m.CreateSandbox(ctx, "dev",
m.WithImage("python"),
m.WithMounts(map[string]m.MountConfig{
"/app/src": m.Mount.Bind("./src", m.MountOptions{Readonly: true, Noexec: true}),
"/app/data": m.Mount.Bind("./data", m.MountOptions{}),
}),
)
msb create python --name dev \
--mount-dir ./src:/app/src:ro,noexec \
--mount-dir ./data:/app/data
Use --mount-file when you want to bind one host file instead of an entire directory.
msb create alpine --name config-reader \
--mount-file ./config.toml:/etc/app/config.toml:ro,nodev
Named volumes are managed by microsandbox and stored by default under ~/.microsandbox/volumes/<name>/. They persist independently of any sandbox, so you can create a volume, populate it, and mount it into different sandboxes over time.
Named volumes can be directory-backed or disk-backed. Directory volumes mount through virtiofs. Disk volumes are raw ext4 disk images managed by microsandbox and mount through virtio-blk.
CLI named mounts are idempotent. -v name:/path and --mount-named name:/path create a directory-backed named volume if it does not already exist, then mount it. If the volume already exists, microsandbox reuses it when the requested storage settings are compatible and errors when they are not. Use --mount-named name:/path:kind=disk,size=20G to create or reuse a disk-backed named volume from the mount flag itself.
SDK named(...) mount helpers are existing-only. They fail if the named volume does not already exist. Use each SDK's explicit named-volume mode API when you want sandbox creation to create or ensure the volume.
import { Volume } from "microsandbox";
const cache = await Volume.builder("pip-cache").create();
cache = await Volume.create("pip-cache")
cache, err := m.CreateVolume(ctx, "pip-cache")
msb volume create pip-cache
Create a disk-backed named volume when a workload needs a real guest block filesystem, such as Docker's data root:
<CodeGroup> ```rust Rust let docker_data = Volume::builder("docker-data") .disk() .size(20.gib()) .create() .await?; ```const dockerData = await Volume.builder("docker-data")
.disk()
.size(20 * 1024)
.create();
docker_data = await Volume.create("docker-data", kind="disk", size_mib=20 * 1024)
dockerData, err := m.CreateVolume(ctx, "docker-data",
m.WithVolumeKind(m.VolumeKindDisk),
m.WithVolumeSize(20 * 1024),
)
msb volume create docker-data --kind disk --size 20G
await using sb = await Sandbox.builder("worker")
.image("python")
.volume("/root/.cache/pip", (m) => m.named(cache.name))
.create();
sb = await Sandbox.create(
"worker",
image="python",
volumes={
"/root/.cache/pip": Volume.named(cache.name),
},
)
sb, err := m.CreateSandbox(ctx, "worker",
m.WithImage("python"),
m.WithMounts(map[string]m.MountConfig{
"/root/.cache/pip": m.Mount.Named(cache.Name(), m.MountOptions{}),
}),
)
msb create python --name worker \
--mount-named pip-cache:/root/.cache/pip
The CLI command above creates pip-cache as a directory-backed named volume if it is missing. To create or reuse a disk-backed named volume while mounting it:
msb create docker:dind --name docker-demo \
--mount-named docker-data:/var/lib/docker:kind=disk,size=20G
Mount the same directory-backed named volume into multiple sandboxes when they need to share files. Use readonly on consumers that should not write. Disk-backed named volumes are attached as block devices; use one writable sandbox at a time, or read-only mounts where sharing is intended.
Directory-backed named volumes are also accessible from the host without a running sandbox, so you can pre-populate a cache or inspect output after the sandbox stops. Disk-backed named volumes store guest data inside disk.raw; host filesystem helpers operate on the managed volume directory, not the filesystem inside the disk image.
const volumes = await Volume.list();
await Volume.remove("old-cache");
volumes = await Volume.list()
await Volume.remove("old-cache")
volumes, err := m.ListVolumes(ctx)
err = m.RemoveVolume(ctx, "old-cache")
msb volume ls
msb volume rm old-cache
Disk-image volumes attach a host disk image as a virtio-blk device and mount its inner filesystem at a guest path. Use them when you want persistent state isolated from ordinary host filesystem metadata, or when distributing a prebuilt filesystem image. The disk format defaults from the file extension (.qcow2, .raw, .vmdk; otherwise raw), and the inner filesystem type is auto-detected unless you set fstype.
await using sb = await Sandbox.builder("seeded")
.image("alpine")
.volume("/seed", (m) => m.disk("./seed.raw").readonly().noexec())
.create();
sb = await Sandbox.create(
"seeded",
image="alpine",
volumes={"/seed": Volume.disk("./seed.raw", readonly=True, noexec=True)},
)
sb, err := m.CreateSandbox(ctx, "seeded",
m.WithImage("alpine"),
m.WithMounts(map[string]m.MountConfig{
"/seed": m.Mount.Disk("./seed.raw", m.DiskOptions{Readonly: true, Noexec: true}),
}),
)
msb create alpine --name seeded \
--mount-disk ./seed.raw:/seed:ro,noexec,fstype=ext4
An in-memory filesystem that disappears when the sandbox stops. Useful for scratch space, build artifacts, or anything that doesn't need to outlive the sandbox.
<CodeGroup> ```rust Rust let sb = Sandbox::builder("worker") .image("alpine") .volume("/tmp/scratch", |v| v.tmpfs().size(100).noexec()) .create() .await?; ```import { Sandbox } from "microsandbox";
await using sb = await Sandbox.builder("worker")
.image("alpine")
.volume("/tmp/scratch", (m) => m.tmpfs().size(100).noexec())
.create();
sb = await Sandbox.create(
"worker",
image="alpine",
volumes={
"/tmp/scratch": Volume.tmpfs(size_mib=100, noexec=True),
},
)
sb, err := m.CreateSandbox(ctx, "worker",
m.WithImage("alpine"),
m.WithMounts(map[string]m.MountConfig{
"/tmp/scratch": m.Mount.Tmpfs(m.TmpfsOptions{SizeMiB: 100, Noexec: true}),
}),
)
msb create alpine --name worker \
--tmpfs /tmp/scratch:100:noexec
Mount flags may be repeated, so a sandbox can combine host directories, individual files, named volumes, disk images, and tmpfs scratch space.
<CodeGroup> ```rust Rust let sb = Sandbox::builder("full") .image("python") .volume("/app/src", |v| v.bind("./src").readonly().noexec()) .volume("/app/pyproject.toml", |v| v.bind("./pyproject.toml").readonly()) .volume("/root/.cache/pip", |v| { v.named("pip-cache") .stat_virtualization(StatVirtualization::Relaxed) }) .volume("/dataset", |v| { v.disk("./dataset.qcow2") .format(DiskImageFormat::Qcow2) .fstype("ext4") .readonly() }) .volume("/tmp", |v| v.tmpfs().size(1024).noexec().nosuid().nodev()) .create() .await?; ```msb create python --name full \
--mount-dir ./src:/app/src:ro,noexec \
--mount-file ./pyproject.toml:/app/pyproject.toml:ro \
--mount-named pip-cache:/root/.cache/pip:stat-virt=relaxed \
--mount-disk ./dataset.qcow2:/dataset:ro,format=qcow2,fstype=ext4 \
--tmpfs /tmp:1G:noexec,nosuid,nodev