Skip to main content
External Linux sandboxes

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.

Clientbuildstores the sandbox idDaytonasandboxactionsexec, filesOne per buildRivet ActorCall

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

OptionDescription
clientA Daytona client. Defaults to new Daytona(), which reads DAYTONA_API_KEY.
createOptions 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.