---
title: Volumes
description: Persist and share data across sandboxes
icon: "hard-drive"
---

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](/sandboxes/filesystem) (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 are `nosuid` and `nodev` inside the guest by default. Add `readonly` / `ro` when the guest should not write, and add `noexec` when files on the mount should not be executed directly. User commands also run without mount-admin privilege, so `ro` and `noexec` cannot be undone with a guest remount. `noexec` does not stop interpreters from reading scripts from the mount, for example `sh /app/src/script.sh`.

## Bind mounts

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?;
```

```typescript TypeScript
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();
```

```python Python
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"),
    },
)
```

```go Go
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{}),
    }),
)
```

```bash CLI
msb create python --name dev \
  -v ./src:/app/src:ro,noexec \
  -v ./data:/app/data
```

</CodeGroup>

## Named volumes

Named volumes are managed by microsandbox and stored at `~/.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.

### Create a volume

<CodeGroup>
```rust Rust
let cache = Volume::builder("pip-cache")
    .quota(1024)
    .create()
    .await?;
```

```typescript TypeScript
import { Volume } from "microsandbox";

const cache = await Volume.builder("pip-cache").quota(1024).create();
```

```python Python
cache = await Volume.create("pip-cache", quota_mib=1024)
```

```go Go
cache, err := m.CreateVolume(ctx, "pip-cache",
    m.WithVolumeQuota(1024),
)
```

```bash CLI
msb volume create pip-cache --size 1024
```

</CodeGroup>

### Mount in a sandbox

<CodeGroup>
```rust Rust
let sb = Sandbox::builder("worker")
    .image("python")
    .volume("/root/.cache/pip", |v| v.named(cache.name()))
    .create()
    .await?;
```

```typescript TypeScript
await using sb = await Sandbox.builder("worker")
    .image("python")
    .volume("/root/.cache/pip", (m) => m.named(cache.name))
    .create();
```

```python Python
sb = await Sandbox.create(
    "worker",
    image="python",
    volumes={
        "/root/.cache/pip": Volume.named(cache.name),
    },
)
```

```go Go
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{}),
    }),
)
```

```bash CLI
msb create python --name worker \
  -v pip-cache:/root/.cache/pip
```

</CodeGroup>

### Share between sandboxes

Named volumes are the simplest way to share data between sandboxes. Mount the same volume into multiple sandboxes: one writes, another reads.

<CodeGroup>
```rust Rust
// Writer
let writer = Sandbox::builder("writer")
    .image("alpine")
    .volume("/data", |v| v.named("shared-data"))
    .create()
    .await?;
writer.shell("echo 'hello' > /data/message.txt").await?;

// Reader (read-only)
let reader = Sandbox::builder("reader")
    .image("alpine")
    .volume("/data", |v| v.named("shared-data").readonly())
    .create()
    .await?;
let output = reader.exec("cat", ["/data/message.txt"]).await?;
// => "hello"
```

```typescript TypeScript
// Writer
await using writer = await Sandbox.builder("writer")
    .image("alpine")
    .volume("/data", (m) => m.named("shared-data"))
    .create();
await writer.shell("echo 'hello' > /data/message.txt");

// Reader (read-only)
await using reader = await Sandbox.builder("reader")
    .image("alpine")
    .volume("/data", (m) => m.named("shared-data").readonly())
    .create();
const output = await reader.exec("cat", ["/data/message.txt"]);
// => "hello"
```

```python Python
# Writer
writer = await Sandbox.create(
    "writer",
    image="alpine",
    volumes={"/data": Volume.named("shared-data")},
)
await writer.shell("echo 'hello' > /data/message.txt")

# Reader (read-only)
reader = await Sandbox.create(
    "reader",
    image="alpine",
    volumes={"/data": Volume.named("shared-data", readonly=True)},
)
output = await reader.exec("cat", ["/data/message.txt"])
# => "hello"
```

```go Go
// Writer
writer, err := m.CreateSandbox(ctx, "writer",
    m.WithImage("alpine"),
    m.WithMounts(map[string]m.MountConfig{
        "/data": m.Mount.Named("shared-data", m.MountOptions{}),
    }),
)
_, err = writer.Shell(ctx, "echo 'hello' > /data/message.txt")

// Reader (read-only)
reader, err := m.CreateSandbox(ctx, "reader",
    m.WithImage("alpine"),
    m.WithMounts(map[string]m.MountConfig{
        "/data": m.Mount.Named("shared-data", m.MountOptions{Readonly: true}),
    }),
)
output, err := reader.Exec(ctx, "cat", []string{"/data/message.txt"})
// => "hello"
```

</CodeGroup>

<Tip>
  When sharing volumes between sandboxes, both sandboxes access the same underlying host directory. There's no built-in locking, so if two sandboxes write to the same file concurrently, you'll get the usual race conditions. Use the read-only flag on the reader side to make intent explicit.
</Tip>

### Host-side access

Named volumes are accessible from the host even without a running sandbox, which is handy for pre-populating data before any sandbox mounts it.

<CodeGroup>
```rust Rust
let vol = Volume::get("pip-cache").await?;
vol.fs().write("/requirements.txt", "requests==2.28.0").await?;
```

```typescript TypeScript
const vol = await Volume.get("pip-cache");
await vol.fs().write("requirements.txt", "requests==2.28.0");
```

```python Python
vol = await Volume.get("pip-cache")
await vol.fs.write("/requirements.txt", b"requests==2.28.0")
```

```go Go
vol, err := m.GetVolume(ctx, "pip-cache")
err = vol.FS().WriteString("requirements.txt", "requests==2.28.0")
```

</CodeGroup>

### Manage volumes

<CodeGroup>
```rust Rust
let volumes = Volume::list().await?;
Volume::remove("old-cache").await?;
```

```typescript TypeScript
const volumes = await Volume.list();
await Volume.remove("old-cache");
```

```python Python
volumes = await Volume.list()
await Volume.remove("old-cache")
```

```go Go
volumes, err := m.ListVolumes(ctx)
err = m.RemoveVolume(ctx, "old-cache")
```

```bash CLI
msb volume ls
msb volume rm old-cache
```

</CodeGroup>

## Disk-image volumes

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`.

<CodeGroup>
```rust Rust
let sb = Sandbox::builder("seeded")
    .image("alpine")
    .volume("/seed", |v| v.disk("./seed.raw").readonly().noexec())
    .create()
    .await?;
```

```typescript TypeScript
await using sb = await Sandbox.builder("seeded")
    .image("alpine")
    .volume("/seed", (m) => m.disk("./seed.raw").readonly().noexec())
    .create();
```

```python Python
sb = await Sandbox.create(
    "seeded",
    image="alpine",
    volumes={"/seed": Volume.disk("./seed.raw", readonly=True, noexec=True)},
)
```

```go Go
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}),
    }),
)
```
</CodeGroup>

## Tmpfs

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?;
```

```typescript TypeScript
import { Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("worker")
    .image("alpine")
    .volume("/tmp/scratch", (m) => m.tmpfs().size(100).noexec())
    .create();
```

```python Python
sb = await Sandbox.create(
    "worker",
    image="alpine",
    volumes={
        "/tmp/scratch": Volume.tmpfs(size_mib=100, noexec=True),
    },
)
```

```go Go
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}),
    }),
)
```

```bash CLI
msb create alpine --name worker \
  --tmpfs /tmp/scratch:100:noexec
```

</CodeGroup>
