---
title: Bootstrap
description: Prepare scripts, patches, and PID 1 before work starts
icon: "power-off"
---

Before a sandbox starts doing real work, you can choose its root storage and prepare its environment. **Flat root disks** trade layer-level deduplication for a direct ext4 root. **Scripts** bundle reusable commands. **Patches** modify the layered rootfs before the VM boots. A **custom init system** (systemd, OpenRC, s6) can run as PID 1 instead of microsandbox's minimal agent.

## Flat OCI rootfs

OCI sandboxes use stitched EROFS lower layers with a writable ext4 upper and guest OverlayFS by default. A flat root disk is an explicit alternative: microsandbox merges the OCI layers into one deterministic ext4 base, caches it by content, creates a private clone for each sandbox, grows that clone to the requested capacity, and mounts it directly as the guest root. The immutable cached base is never attached writable.

<CodeGroup>
```rust Rust
use microsandbox::sandbox::FlatClone;
use microsandbox::Sandbox;

let sb = Sandbox::builder("worker")
    .image("python:3.12")
    .root_disk_with(|disk| disk.flat().size(8192).clone_strategy(FlatClone::Auto))
    .create()
    .await?;
```

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

await using sb = await Sandbox.builder("worker")
    .image("python:3.12")
    .rootDisk((disk) => disk.flat().size(8192).cloneStrategy("auto"))
    .create();
```

```python Python
from microsandbox import FlatClone, Image, RootDisk, Sandbox

sb = await Sandbox.create(
    "worker",
    image=Image.oci(
        "python:3.12",
        root_disk=RootDisk.flat(8192, clone=FlatClone.AUTO),
    ),
)
```

```go Go
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("python:3.12"),
    m.WithRootDisk(m.RootDisk.Flat(m.RootDiskFlatOptions{
        SizeMiB: 8192,
        Clone:   m.FlatCloneAuto,
    })),
)
```
</CodeGroup>

`auto` first asks the host filesystem for a native copy-on-write clone and falls back to an independent sparse copy when cloning is unsupported. Use `copy` when independence from the base's physical extents matters more than provisioning latency, or `reflink` when clone support is an operational requirement and failure is preferable to fallback. Linux uses `FICLONE`, macOS uses `clonefile`, and Windows uses block cloning only on volumes that advertise block refcounting.

The normal layered root remains the default and retains layer-level deduplication. Flat v1 is local-only, ext4-only, and incompatible with rootfs patches and snapshots; unsupported combinations fail explicitly.

See [Optimization](/sandboxes/optimization#storage-layout-layered-or-flat) for workload tradeoffs, host filesystem expectations, and how to verify the clone strategy actually used.

## Scripts

Scripts are files mounted at `/.msb/scripts/` inside the sandbox. The directory is on `PATH`, so each script is callable by name through `exec()` or `shell()`.

It provides a clean way to bundle setup procedures or entry points with a sandbox without baking them into the image.

<CodeGroup>
```rust Rust
use indoc::indoc;
use microsandbox::Sandbox;

let sb = Sandbox::builder("worker")
    .image("ubuntu")
    .script("setup", indoc! {"
        #!/bin/bash
        apt-get update && apt-get install -y python3 curl
    "})
    .script("start", indoc! {"
        #!/bin/bash
        exec python3 /app/main.py
    "})
    .create()
    .await?;

sb.shell("setup").await?;
let output = sb.shell("start").await?;
```

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

await using sb = await Sandbox.builder("worker")
    .image("ubuntu")
    .script("setup", "#!/bin/bash\napt-get update && apt-get install -y python3 curl")
    .script("run",   "#!/bin/bash\nexec python3 /app/main.py")
    .create();

await sb.shell("setup");
const output = await sb.shell("run");
```

```python Python
from microsandbox import Sandbox

sb = await Sandbox.create(
    "worker",
    image="ubuntu",
    scripts={
        "setup": "#!/bin/bash\napt-get update && apt-get install -y python3 curl",
        "start": "#!/bin/bash\nexec python3 /app/main.py",
    },
)

await sb.shell("setup")
output = await sb.shell("start")
```

```go Go
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("ubuntu"),
    m.WithScripts(map[string]string{
        "setup": "#!/bin/bash\napt-get update && apt-get install -y python3 curl",
        "start": "#!/bin/bash\nexec python3 /app/main.py",
    }),
)

_, err = sb.Shell(ctx, "setup")
output, err := sb.Shell(ctx, "start")
```

</CodeGroup>

## Patches

Patches modify the rootfs **before the VM boots**. Write config files, copy directories from the host, create symlinks, append to existing files, remove things you don't need. The base image stays untouched since patches are written to the writable layer on top.

Patches are applied in order and work with OCI images and bind-mounted rootfs. They're not supported with disk image roots (QCOW2, Raw).

<Tip>
  By default, patching a path that already exists in the image will error. Pass `replace: true` on the operation to allow it. `Mkdir` and `Remove` are idempotent and won't error either way.
</Tip>

<CodeGroup>
```rust Rust
use microsandbox::Sandbox;

let sb = Sandbox::builder("worker")
    .image("alpine")
    .patch(|p| p
        .text("/etc/greeting.txt", "Hello from a patched rootfs!\n", None, false)
        .text("/etc/motd", "Custom message of the day.\n", None, true) // replace existing
        .mkdir("/app", Some(0o755))
        .text("/app/config.json", r#"{"debug": true}"#, Some(0o644), false)
        .copy_file("./cert.pem", "/etc/ssl/cert.pem", None, false)
        .append("/etc/hosts", "127.0.0.1 myapp.local\n")
    )
    .create()
    .await?;
```

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

await using sb = await Sandbox.builder("worker")
    .image("alpine")
    .patch((p) => p
        .text("/etc/greeting.txt", "Hello from a patched rootfs!\n")
        .text("/etc/motd", "Custom message of the day.\n", { replace: true })
        .mkdir("/app", { mode: 0o755 })
        .text("/app/config.json", '{"debug": true}', { mode: 0o644 })
        .copyFile("./cert.pem", "/etc/ssl/cert.pem")
        .append("/etc/hosts", "127.0.0.1 myapp.local\n"),
    )
    .create();
```

```python Python
from microsandbox import Patch, Sandbox

sb = await Sandbox.create(
    "worker",
    image="alpine",
    patches=[
        Patch.text("/etc/greeting.txt", "Hello from a patched rootfs!\n"),
        Patch.text("/etc/motd", "Custom message of the day.\n", replace=True),
        Patch.mkdir("/app", mode=0o755),
        Patch.text("/app/config.json", '{"debug": true}', mode=0o644),
        Patch.copy_file("./cert.pem", "/etc/ssl/cert.pem"),
        Patch.append("/etc/hosts", "127.0.0.1 myapp.local\n"),
    ],
)
```

```go Go
mode755 := uint32(0o755)
mode644 := uint32(0o644)
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("alpine"),
    m.WithPatches(
        m.Patch.Text("/etc/greeting.txt", "Hello from a patched rootfs!\n", m.PatchOptions{}),
        m.Patch.Text("/etc/motd", "Custom message of the day.\n", m.PatchOptions{Replace: true}),
        m.Patch.Mkdir("/app", m.PatchOptions{Mode: &mode755}),
        m.Patch.Text("/app/config.json", `{"debug": true}`, m.PatchOptions{Mode: &mode644}),
        m.Patch.CopyFile("./cert.pem", "/etc/ssl/cert.pem", m.PatchOptions{}),
        m.Patch.Append("/etc/hosts", "127.0.0.1 myapp.local\n"),
    ),
)
```

</CodeGroup>

### Available operations

The patch builder appends operations in the order you call them; calls are chainable. Available operations across SDKs: `text`, `file`, `mkdir`, `append`, `copyFile` / `copy_file`, `copyDir` / `copy_dir`, `symlink`, `remove`.

For per-language signatures and option shapes, see the SDK references:

- Rust: [`PatchBuilder`](/sdk/rust/sandbox#patchbuilder)
- TypeScript: [`PatchBuilder`](/sdk/typescript/sandbox#patchbuilder)
- Python: [`Patch`](/sdk/python/sandbox#patch) (factory) and [`PatchConfig`](/sdk/python/sandbox#patchconfig) (the value type)
- Go: [`Patch`](/sdk/go/sandbox#patch) (helpers) and [`PatchConfig`](/sdk/go/sandbox#patchconfig) (the value type)

## Transparent huge pages

Transparent huge pages (THP) let the guest kernel back eligible anonymous memory with larger pages. This can reduce address-translation overhead for large memory working sets, especially inside a virtual machine, but `always` may allocate memory in 2 MiB units for sparsely touched mappings. Microsandbox therefore defaults to the density-conscious `madvise` policy and lets each sandbox choose its policy at creation time.

The selected value is applied as the Linux `transparent_hugepage=` kernel boot parameter before agentd or the user workload starts. It is persisted with the sandbox and takes effect again on every start. It cannot be changed with `msb modify`; create or replace the sandbox to select another policy.

<CodeGroup>
```rust Rust
use microsandbox::{Sandbox, sandbox::TransparentHugePagePolicy};

let sb = Sandbox::builder("memory-worker")
    .image("python:3.12")
    .thp(TransparentHugePagePolicy::Always)
    .create()
    .await?;
```

```typescript TypeScript
const sb = await Sandbox.builder("memory-worker")
  .image("python:3.12")
  .thp("always")
  .create();
```

```python Python
sb = await Sandbox.create(
    "memory-worker",
    image="python:3.12",
    thp="always",
)
```

```go Go
sb, err := m.CreateSandbox(ctx, "memory-worker",
    m.WithImage("python:3.12"),
    m.WithTHP(m.THPAlways),
)
```

```bash CLI
msb create python:3.12 --name memory-worker --thp always
```
</CodeGroup>

Use `always` when the workload has large, densely touched anonymous allocations and memory bandwidth or TLB pressure matters. Keep `madvise` for general-purpose and high-density sandboxes. Use `never` for diagnostics or workloads that must avoid transparent huge pages entirely.

## Custom init system

By default the microsandbox agent runs as PID 1 inside the guest: small, fast, minimal. For workloads that expect a real init (systemd, OpenRC, s6, runit, etc.), `--init` hands PID 1 over to the init binary of your choice.

Common reasons to opt in: long-lived daemons, system service tests, anything that talks to dbus or expects `systemctl` to work.

Use `auto` to pick a known init binary from the image, or pass an absolute path when you need to pin the entry point for reproducible CI. When an OCI image declares a known init path as the first ENTRYPOINT token, such as `/init` in s6-overlay images, `auto` hands PID 1 to that path. For attached `msb run`, microsandbox preserves the OCI launch contract by passing the remaining ENTRYPOINT plus CMD or trailing command to that init instead of direct-executing the wrapper through agentd. Otherwise, `auto` falls back to probing common distro paths: `/sbin/init`, `/lib/systemd/systemd`, and `/usr/lib/systemd/systemd`.

<CodeGroup>
```rust Rust
use microsandbox::Sandbox;

let sb = Sandbox::builder("worker")
    .image("ghcr.io/superradcompany/debian-systemd:12")
    .memory(1024)
    .cpus(2)
    .init("auto")
    .create()
    .await?;
```

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

await using sb = await Sandbox.builder("worker")
    .image("ghcr.io/superradcompany/debian-systemd:12")
    .memory(MiB(1024))
    .cpus(2)
    .init("auto")
    .create();
```

```python Python
from microsandbox import Sandbox

sb = await Sandbox.create(
    "worker",
    image="ghcr.io/superradcompany/debian-systemd:12",
    memory=1024,
    cpus=2,
    init="auto",
)
```

```go Go
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("ghcr.io/superradcompany/debian-systemd:12"),
    m.WithMemory(1024),
    m.WithCPUs(2),
    m.WithInit(m.Init.Auto()),
)
```

```bash CLI
msb run ghcr.io/superradcompany/debian-systemd:12 \
  -m 1G -c 2 \
  --init auto \
  -- bash
```
</CodeGroup>

To verify the handoff worked, check `/proc/1/comm` inside the sandbox:

```bash
$ msb run ghcr.io/superradcompany/debian-systemd:12 --init auto -- cat /proc/1/comm
systemd
```

If `--init=auto` cannot find an init binary from the image ENTRYPOINT or the guest-side probe list, boot fails with a clear error in `kernel.log`. Use an explicit path when you know where the init lives.

### Argv and env

Pass extra argv and env to the init:

- **Rust / TypeScript**: `init_with(...)` / `initWith(...)`.
- **Python**: pass an `InitConfig` to `init=`.
- **CLI**: repeat `--init-arg` once per entry, and `--init-env KEY=VAL` once per env var.

`argv[0]` is the init command; extra argv entries are appended after it. Env is merged on top of the inherited environment.

<CodeGroup>
```rust Rust
let sb = Sandbox::builder("worker")
    .image("ghcr.io/superradcompany/debian-systemd:12")
    .init_with("/lib/systemd/systemd", |i| i
        .args(["--unit=multi-user.target"])
        .env("container", "microsandbox"))
    .create()
    .await?;
```

```typescript TypeScript
await using sb = await Sandbox.builder("worker")
    .image("ghcr.io/superradcompany/debian-systemd:12")
    .initWith("/lib/systemd/systemd", (i) => i
        .args(["--unit=multi-user.target"])
        .env("container", "microsandbox"))
    .create();
```

```python Python
from microsandbox import InitConfig, Sandbox

sb = await Sandbox.create(
    "worker",
    image="ghcr.io/superradcompany/debian-systemd:12",
    init=InitConfig(
        cmd="/lib/systemd/systemd",
        args=("--unit=multi-user.target",),
        env={"container": "microsandbox"},
    ),
)
```

```go Go
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("ghcr.io/superradcompany/debian-systemd:12"),
    m.WithInit(m.Init.Cmd("/lib/systemd/systemd",
        m.InitOptions{
            Args: []string{"--unit=multi-user.target"},
            Env:  map[string]string{"container": "microsandbox"},
        },
    )),
)
```

```bash CLI
msb run ghcr.io/superradcompany/debian-systemd:12 \
  --init /lib/systemd/systemd \
  --init-arg --unit=multi-user.target \
  --init-env container=microsandbox \
  -- bash
```
</CodeGroup>

### Picking an image

Most slim Docker base images do not include systemd or another full init. Use an image that ships the init you want, or build a small custom image that installs it.

`--init` controls PID 1. `--entrypoint` and the trailing command normally control the workload you run after boot, so the two can be combined. An image-declared init entrypoint such as `/init` is the special case. With `--init auto` and attached `msb run`, microsandbox passes the trailing command to PID 1 as part of the image's own launch contract.
