> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirage.strukto.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Cloudflare Container

> Run captured programs in a Cloudflare Container, from its Durable Object or from anywhere through a relay.

The `cloudflare_container` machine provider reaches a
[Cloudflare Container](https://developers.cloudflare.com/containers/),
and runs the programs it captures in it. A container belongs to a
Durable Object, which decides when it starts and stops. Mirage reaches it in
one of two ways:

* **In the Durable Object**: a workspace on Cloudflare Workers hands the
  handle its own `ctx.container`. TypeScript only.
* **Through a relay**: you deploy a `ContainerRelay` Durable Object with the
  container, and a workspace anywhere reaches it by `url` and `token`.
  Python, Node, browsers and other Workers.

## Installation

> [tab] Python
>
> ```bash theme={null}
> pip install 'mirage-ai[ssh]'
> ```

> [tab] TypeScript
>
> ```bash theme={null}
> npm install @struktoai/mirage-cloudflare @struktoai/mirage-core
> ```

The `ssh` extra serves the workspace into the container with
`mount_type: ssh`. The relay itself is a Worker, so deploying it needs
`@struktoai/mirage-cloudflare` whichever language the workspace runs in.

Start the image from a Debian or Ubuntu base, which has the shell tools
Mirage's own lines use. Add the interpreters it runs, here `python3` and
`nodejs`, and `sshfs` and `fuse3` for `mount_type: ssh`:

```dockerfile Dockerfile theme={null}
FROM ubuntu:24.04
RUN apt-get update \
 && apt-get install -y --no-install-recommends fuse3 sshfs python3 nodejs \
 && rm -rf /var/lib/apt/lists/*
CMD ["sleep", "infinity"]
```

Programs do not see the image's `ENV` lines: a program starts from
Cloudflare's own `PATH` and `HOME=/root`, with the machine's `env` and the
session's environment on top. Put anything a program needs, such as a
`PATH` entry for a tool installed outside `/usr/local/bin` or `/usr/bin`, in
the machine's `env`.

## Usage through a relay

Deploy a Worker whose Durable Object class extends `ContainerRelay`, and
route requests to it with `relayFetch`:

```typescript src/relay.ts theme={null}
import { ContainerRelay, relayFetch } from '@struktoai/mirage-cloudflare'

export class Relay extends ContainerRelay {}

export default {
  fetch: (request: Request, env: { RELAY: DurableObjectNamespace }) =>
    relayFetch(request, env.RELAY),
}
```

```jsonc wrangler.jsonc theme={null}
{
  "name": "mirage-relay",
  "main": "src/relay.ts",
  "compatibility_flags": ["nodejs_compat"],
  "containers": [{ "class_name": "Relay", "image": "./Dockerfile", "max_instances": 5 }],
  "durable_objects": { "bindings": [{ "name": "RELAY", "class_name": "Relay" }] },
  "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Relay"] }],
}
```

Set the token every call must carry, then deploy:

```bash theme={null}
openssl rand -hex 32 | npx wrangler secret put MIRAGE_RELAY_TOKEN
npx wrangler deploy
```

A relay without `MIRAGE_RELAY_TOKEN` refuses every call. The last path
segment before the call names the container: each `<name>` in
`https://mirage-relay.<account>.workers.dev/<name>` is a Durable Object, and
a container, of its own.

Point a machine at one:

> [tab] Python
>
> ```python theme={null}
> from mirage import RAMVFS, MountMode, Workspace
>
> ws = Workspace(
>     {"/data": RAMVFS()},
>     mode=MountMode.EXEC,
>     machines={
>         "box": {
>             "provider": "cloudflare_container",
>             "url": "https://mirage-relay.example.workers.dev/box",
>             "token": "relay-token",
>             "mount_type": "ssh",
>             "captures": ["@python", "@external"],
>         }
>     },
> )
> await ws.shell("python3 -c 'print(1 + 1)'")
> ```

> [tab] TypeScript
>
> ```typescript theme={null}
> import { MountMode, RAMVFS, Workspace } from '@struktoai/mirage-node'
>
> const ws = new Workspace(
>   { '/data': new RAMVFS() },
>   {
>     mode: MountMode.EXEC,
>     machines: {
>       box: {
>         provider: 'cloudflare_container',
>         url: 'https://mirage-relay.example.workers.dev/box',
>         token: 'relay-token',
>         mountType: 'ssh',
>         captures: ['@python', '@external'],
>       },
>     },
>   },
> )
> await ws.shell("python3 -c 'print(1 + 1)'")
> ```
>
> In a browser, import `@struktoai/mirage-core/machine/cloudflare_container/handle`
> once to register the provider, and use `mountType: 'fuse'` or leave it
> unset.

The relay starts the container with the image its wrangler config names. To
start it with options of your own, such as `enableInternet: false`, override
`startContainer`:

```typescript theme={null}
export class Relay extends ContainerRelay {
  protected override startContainer(): Promise<void> {
    const container = this.container()
    if (!container.running) container.start({ enableInternet: false })
    return Promise.resolve()
  }
}
```

## Usage in the Durable Object

A workspace that already runs in a Durable Object builds the handle from its
own `ctx.container` and a function that starts the container when it is not
running:

```typescript src/worker.ts theme={null}
import { DurableObject } from 'cloudflare:workers'
import { ContainerMachine, MountMode, RAMVFS, Workspace } from '@struktoai/mirage-cloudflare'

export class WorkspaceObject extends DurableObject {
  private readonly ws = new Workspace(
    { '/data': new RAMVFS() },
    {
      mode: MountMode.EXEC,
      machines: {
        box: new ContainerMachine(
          'box',
          { mountType: 'ssh', captures: ['@external', '@python', '@js'] },
          this.ctx.container,
          async () => {
            if (!this.ctx.container?.running) this.ctx.container?.start({ enableInternet: false })
          },
        ),
      },
    },
  )

  async fetch(request: Request): Promise<Response> {
    const { command } = (await request.json()) as { command: string }
    const result = await this.ws.shell(command)
    return Response.json({ exit: result.exitCode, stdout: result.stdoutText })
  }
}
```

Give the Durable Object class the container in your wrangler config, as the
relay's is above, and set the Worker up as the
[Cloudflare quickstart](https://docs.mirage.strukto.ai/typescript/cloudflare-quickstart.md) does.

Mirage calls the start function the first time it reaches the machine, and
again the first time after the container has stopped: for a line that runs
there, and for `ps`, `kill` and the like when they ask it. An error it
throws, such as a quota of your own, fails that line with the error's
message.

## Configuration

| Field | Description |
| - | - |
| `url` | The relay route that names the container. Leave it out in the Durable Object. |
| `token` | The relay's `MIRAGE_RELAY_TOKEN`. Required with `url`. |
| `env` | Environment set for every program. |
| `mount_type` | How the container sees the workspace: `ssh`, `fuse` or `both`, as on [every machine](https://docs.mirage.strukto.ai/home/setup/machine/overview.md#connecting-the-workspace). |

The container's image, instance type and network belong to the Durable
Object that starts it, so none of them is a field. An entry with neither
`url` nor a container handed in fails its first line and says how to reach
one.

## Connecting the workspace

Use [`mount_type: ssh`](https://docs.mirage.strukto.ai/home/setup/machine/overview#connecting-the-workspace):
Mirage serves the workspace's mounts through the container's exec pipe, over
the relay when there is one, and the container mounts each with `sshfs`. No
port, key or network is needed in the container, so it works with
`enableInternet: false`.

## Notes

* The container's own disk is scratch: it is gone when the container stops.
  Files a program should keep belong in a mounted folder.
* If the container stops while a line runs, the line fails: it exits 129 and
  says the container stopped, or, when the Durable Object destroys the
  container itself, it may exit 137. The next line starts it again. A relay
  connection that drops under a line fails it the same way.
* The first line after a start waits for the container to answer, up to 60
  seconds. Most starts answer within a second; a start that never comes up
  fails that line, and the next line starts the container again.
* Output comes back as the program wrote it, binary included, with stdout
  and stderr separate. Without piped input, stdin starts closed.
* Closing the workspace leaves a running container running and starts no
  stopped one. Stop it from the Durable Object.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.