---
title: Overview
description: Run captured lines whole inside a sandbox (Docker, smolvm, Daytona, e2b, or any machine over SSH) you provision yourself.
icon: server
---

The `pyodide` and `monty` runtimes run `python3` in-process on your machine.
A **sandbox runtime** does the opposite: it takes a whole command line and
runs it inside a separate machine, a local container or microVM or a cloud
sandbox. Reach for it when a line needs a real OS, heavy packages, or a GPU
that the in-process interpreters cannot give it.

Five sandbox runtimes ship from `@struktoai/mirage-node`, all with the same
surface:

| Runtime | Connects to | Config |
| --- | --- | --- |
| [`docker`](/typescript/runtime/sandbox/docker) | A container you started (`docker run`) | `container` |
| [`smolvm`](/typescript/runtime/sandbox/smolvm) | A [smolvm](https://smolmachines.com) microVM you started | `machine` |
| [`daytona`](/typescript/runtime/sandbox/daytona) | A [Daytona](https://www.daytona.io) sandbox you created | `sandboxId`, `apiKey` |
| [`e2b`](/typescript/runtime/sandbox/e2b) | An [E2B](https://e2b.dev) sandbox you created | `sandboxId`, `apiKey` |
| [`ssh`](/typescript/runtime/sandbox/ssh) | Any machine running sshd | `host`, `username`, `identityFile` |

`docker` and `smolvm` run on your own machine, Daytona and e2b in the
cloud, and `ssh` reaches anything you can already `ssh` into. The isolation
differs too: a container shares your kernel, a smolvm microVM boots its
own, and an ssh machine is whatever machine you point it at.

<Note>
  Looking for Sandlock? Mirage's
  [Sandlock adapter](/python/runtime/sandbox/sandlock) is Python-only today.
  It confines the host `python3` interpreter rather than arbitrary captured
  lines, and the matching TypeScript adapter has not landed yet.
</Note>

## Routing: what goes to the sandbox

A sandbox runtime does not claim every line. Each runtime **captures** a set
of commands; a captured line runs in the sandbox, everything else stays on
the local vfs. You list the runtimes in order, and `vfs` is the catch-all:

```yaml
runtimes:
  - name: daytona
    captures: ["python3", "pip"] # these run in the sandbox
  - vfs                          # ls, grep, cat, ... stay local
```

So `grep`, `cat`, and `ls` run locally as always, while `python3 train.py`
runs on the cloud box. Mirage never creates, provisions, or deletes
sandboxes: **you bring your own** (a running container or microVM, or a live
Daytona or E2B sandbox), the first captured line connects to it, and every
captured line execs inside it verbatim.

## The workspace inside the sandbox

For the job to see your mounts as ordinary files, the sandbox must serve
the workspace itself, and provisioning that is yours, like everything else
about the sandbox. Run Mirage inside it, with the **same mounts at the
same prefixes** as the host workspace, each FUSE-mounted at its own
prefix:

```yaml
# sandbox.yaml, inside the sandbox
mounts:
  /data:
    resource: s3
    config:
      bucket: my-bucket
      aws_access_key_id: ...
      aws_secret_access_key: ...
    backend: fuse
    mountpoint: /data
```

```bash
mirage workspace create sandbox.yaml # entrypoint, or once by hand
```

A read then pulls from the backend and a write streams straight back to
it, with no upload or sync step: inside the sandbox, `/data` backed by S3
is a real directory. Because you write the sandbox-side config, it can
differ from the host's where it should: the endpoint that is
`127.0.0.1:9000` on your laptop is `host.docker.internal:9000` from inside
a container, credentials can be scoped down, and none of it ever travels
over the provider's exec API. The flip side is that keeping the prefixes
in step with the host workspace is your job; a sandbox serving different
mounts fails loud only when a path misses.

This needs an image with `fuse3` and Mirage plus your backends installed
(see [The sandbox image](#the-sandbox-image)). The in-sandbox mount is
served by the Python Mirage build, so the image is the same for both
language SDKs.

<Note>
  The `ssh` provider can skip all of this when the files live on the ssh
  machine itself: mount them over the ssh resource at a prefix equal to
  their remote absolute path, and captured lines open them natively with no
  FUSE and no remote Mirage. See
  [Skip the FUSE](/typescript/runtime/sandbox/ssh#skip-the-fuse) on the SSH
  page.
</Note>

## Paths inside the sandbox

Mirage rewrites nothing: the line, its cwd, and every path in it pass
through verbatim. With the sandbox serving the same prefixes, `cd /data`
then `python3 train.py` works, and so does an absolute path like
`python3 /data/train.py`, because `/data` means the same thing on both
sides.

## The sandbox image

The sandbox needs `fuse3` plus Mirage with the backends you mount. The repo
ships a Dockerfile that builds this image from the current checkout, so it
always matches your code:

```bash
docker build -f docker/sandbox/Dockerfile --target fuse -t mirage-python-fuse .
```

By default it installs every mountable backend plus fuse. Narrow it to just
the backends you use for a smaller image, or extend it as a base:

```bash
docker build -f docker/sandbox/Dockerfile --target fuse \
  --build-arg MIRAGE_EXTRAS=s3,postgres -t mirage-fuse-lean .
```

The one image is the shared base for every machine provider: run it directly
under Docker or smolvm, use it as a Daytona `image`/`snapshot` source, or use
it as an E2B template base. Continue with the provider page in the Sandbox
sidebar for connection and configuration details.

## Selecting in YAML

Sandbox runtimes are ordinary `runtimes` entries: a name, its captures, and
a `config` block describing the machine (mirroring a mount's `config`
block), with `vfs` as the in-process catch-all. Only the selected runtime
consumes its entry, so one file stays portable:

```yaml
runtimes:
  - name: docker
    captures: ["python3"]
    config:
      container: my-sandbox # started with your own `docker run`
  - vfs

mounts:
  /data:
    resource: s3
    config:
      bucket: ${AWS_S3_BUCKET}
      region: ${AWS_DEFAULT_REGION}
      aws_access_key_id: ${AWS_ACCESS_KEY_ID}
      aws_secret_access_key: ${AWS_SECRET_ACCESS_KEY}
```

## Resource limits

A captured line is a command like any other: the same `command_limits`
that guard `cat` or `grep` guard `python3`, including in the sandbox. A run
that exceeds `timeoutSeconds` answers exit 124; `maxBytes` and `maxLines`
cap its output the same way. There is no sandbox-specific limit surface.

<Warning>
**In a sandbox a limit bounds the wait, not the run.** Exit 124 means
mirage stopped waiting; the command keeps going inside the sandbox. Mirage
holds only a client, and the process lives in the sandbox's own namespace,
so dropping that client never signals it — a `docker exec` command is still
burning CPU after its client is killed. Only the provider's own timeout
reclaims it, and mirage does not set one today:

- **Docker and smolvm** have no server-side timeout: a runaway runs until you
  stop it or the container or microVM exits.
- **E2B** applies its SDK default of 60 seconds, which cuts both ways — a
  runaway stops there, and so does a legitimate build or query that needs
  longer.
- **Daytona** defaults to no timeout, so a runaway keeps running, and
  billing, until you stop it.

Cancelling a line has the same shape: it releases mirage's side and leaves
the sandbox side running. Treat `commandLimits` here as a bound on
waiting rather than on execution, and set a lifecycle limit (idle-stop,
auto-delete) on the sandbox itself.
</Warning>

## Serving as the policy engine

A sandbox runs whole lines; it does not evaluate expressions, so it cannot
run [policy scripts](/home/policy-engine). To make one eligible, subclass it
and implement `eval` with your own transport. The
[docker eval examples](https://github.com/strukto-ai/mirage/tree/main/examples)
do exactly that by piping a small harness to the container's `python3 -`.
