Daytona
Give each Rivet Actor its own Daytona sandbox.
Daytona runs sandboxes created from snapshots, with a stop-and-start lifecycle that keeps the filesystem between runs. Through @rivet-dev/sandbox-adapter, each Actor creates a Daytona sandbox, runs commands and uses files in it, stops it while the Actor sleeps, and deletes it when the Actor is destroyed.
Quickstart
Install
npm install rivetkit @rivet-dev/sandbox-adapter @daytonaio/sdk
Configure Daytona
Create an API key in the Daytona dashboard and put it in .env. It stays on the node and never enters the sandbox.
DAYTONA_API_KEY=
Define the Actor
Each build Actor owns one Daytona sandbox. The first run, saveFile, or readFile creates it; later calls connect to the same sandbox.
import { posix } from "node:path";
import type { Sandbox, SandboxActorContext } from "@rivet-dev/sandbox-adapter";
import { daytonaProvider } from "@rivet-dev/sandbox-adapter/daytona";
import { actor, event, setup } from "rivetkit";
// One provider object serves every Actor of this definition.
const provider = daytonaProvider();
interface BuildState {
sandboxId: string | null;
}
export const build = actor({
state: { sandboxId: null } as BuildState,
events: {
// Command output, as it arrives.
output: event<string>(),
},
actions: {
run: async (c, command: string) => {
const sandbox = await openSandbox(c, c.state);
const decoder = new TextDecoder();
return sandbox.exec(command, {
cwd: sandbox.cwd,
timeoutMs: 60_000,
// Stops the command if the Actor shuts down.
signal: c.abortSignal,
onData: (chunk) => c.broadcast("output", decoder.decode(chunk, { stream: true })),
});
},
saveFile: async (c, path: string, content: string) => {
const sandbox = await openSandbox(c, c.state);
const target = posix.resolve(sandbox.cwd, path);
await sandbox.mkdir(posix.dirname(target));
await sandbox.writeFile(target, content);
},
readFile: async (c, path: string) => {
const sandbox = await openSandbox(c, c.state);
const bytes = await sandbox.readFile(posix.resolve(sandbox.cwd, path));
return new TextDecoder().decode(bytes);
},
},
// The sandbox stops while the Actor sleeps and is removed along with the Actor.
onSleep: async (c) => {
if (c.state.sandboxId) await provider.suspend?.(c, c.state.sandboxId);
},
onDestroy: async (c) => {
if (c.state.sandboxId) await provider.destroy?.(c, c.state.sandboxId);
},
});
// Connects to this Actor's sandbox. The first call creates it.
async function openSandbox(c: SandboxActorContext, state: BuildState): Promise<Sandbox> {
const existing = state.sandboxId ? await provider.connect(c, state.sandboxId) : undefined;
if (existing) return existing;
state.sandboxId = await provider.create(c);
const sandbox = await provider.connect(c, state.sandboxId);
if (!sandbox) throw new Error(`Sandbox ${state.sandboxId} was not found right after it was created.`);
return sandbox;
}
export const registry = setup({ use: { build } });
registry.start();
Options
| Option | Description |
|---|---|
client | A Daytona client. Defaults to new Daytona(), which reads DAYTONA_API_KEY. |
create | Options for Daytona’s create, such as the snapshot. |
The working directory is the sandbox’s own working directory. suspend stops the sandbox while the Actor sleeps, and connect starts it again the next time the Actor uses it. destroy deletes it. Daytona returns stdout and stderr together in stdout, leaves stderr empty, and calls onData once when the command ends.
The Sandbox API, lifecycle, and the other providers are in the sandbox adapter guide.