Skip to content

Capsules

A capsule is a recipe for a throwaway Linux machine: which packages to install, which users to create, which file systems and secrets to mount, and which commands to run when it starts. Starting a capsule gives you that machine to run commands in. It ends when its commands finish, when you stop it, or when it runs out of time.

Use capsules to run real code: crunch a CSV, build a project, convert files, call a command-line tool.

capsules/sales-report.mtr
// A capsule is a short-lived Linux sandbox. This one mounts the sales-data file system,
// runs a Python script on the CSV in it, and stops when the script finishes.
autoTerminate = true
maxDuration = "00:05:00"
// The file system must exist before you deploy: create it and upload the data first.
vfs = [
{
vfs = @ref("sales-data")
mountName = "data"
}
]
startup = [
{
command = "python3"
arguments = ["/mnt/data/summarise.py"]
wait = true
maxDuration = "00:02:00"
}
]
PropertyWhat it does
packagesextra Debian packages to install with apt, for example ["jq", "sqlite3"]
usersLinux users to create: [ { name = "builder" } ]
vfsfile systems to mount: each one appears at /mnt/<mountName>
secretssecrets to mount: each one is written to the file /mnt/secrets/<mountName>
startupcommands to run in order: [ { command arguments runAsUser maxDuration wait } ]
autoTerminatefinish as soon as the startup commands are done
maxDurationthe longest it may live, as "hh:mm:ss" (10 minutes by default), counted from when it starts. A capsule still starting up then fails; a ready one finishes, and a command still running is stopped

The machine already has Python 3, Node.js 22, .NET 8, Java 17, git, build tools, pandoc and ffmpeg.

A capsule without autoTerminate stays up and takes commands one at a time. Start it, send it commands, and stop it when you’re done. execAndWait sends a command and waits for it to end, then gives you its exit code and output. This is the workbench capsule of tutorial 21, which builds it step by step:

import { Maitento } from "@maitento/sdk";
// Run sandbox.ts first: the workbench capsule mounts the sales-data file system it creates.
const mt = new Maitento();
await mt.deploy("../maitento");
// Start the workbench capsule. It stays up, so it can take commands one at a time.
const capsule = await mt.capsules.start("workbench");
// Run a command as the sandbox user, and wait for it to end. Each argument reaches the program as one word.
const listing = await mt.capsuleRuns.execAndWait(capsule.id, {
command: "ls",
arguments: ["-l", "/tmp/data"],
runAsUser: "sandbox",
});
console.log(`exit code ${listing.exitCode}\n${listing.outputStandard}`);
// A command that fails is a result too. The mounted file system belongs to the default user, so sandbox can't read it.
const denied = await mt.capsuleRuns.execAndWait(capsule.id, {
command: "cat",
arguments: ["/mnt/data/sales.csv"],
runAsUser: "sandbox",
});
console.log(`exit code ${denied.exitCode}\n${denied.outputError}`);
await mt.capsuleRuns.terminate(capsule.id);
  • A command that fails is a result. It comes back with status Error and its exit code; only a capsule that ends before the command does (stopped, or out of time) is an error: CapsuleCommandAbortedError.
  • execAndWait hears the end as it happens when you have an event stream open, and otherwise checks every second or two. In C# it’s CapsuleProcesses.RunCommandAndWaitAsync, in Python capsule_runs.exec_and_wait.
  • exec only sends the command. Use it to start something and read it later with getCommand. Pass async: true to let the capsule move on to the next command without waiting for this one.
  • You can send commands while the capsule is still starting. They run, in order, once it’s ready.
  • In an app, CapsuleExec sends a command and CapsuleCommandAwait waits for it.

Each command records its exit code, its standard output and its error output. Of each output, the last 65,536 characters are kept; when there was more, outputStandardFullLength or outputErrorFullLength says how long it was.

  • Create file systems and secrets before you deploy a capsule that mounts them.
  • Output from startup commands isn’t kept. Write results to a mounted file system, as the example does, or run the work as a command after start-up.
  • Each argument is one word, exactly as you give it. Spaces, quotes and $ in an argument reach the program unchanged, so ["hello world"] stays one argument. The command itself is shell text: to pipe or redirect, run sh with the arguments ["-c", "<your command line>"], or put the whole line in the command.
  • Secrets are readable by the default user only. If you run commands as another user, copy the secret first.
  • Mounted file systems belong to the default user too (maitento, mode 770), so a command run as another user can’t read them. Copy what that user needs somewhere it can read, as tutorial 21 does.
  • The default user is signed in to mt as you, or as whoever started the capsule, with the capsule’s namespace already set. mt is on its PATH, so a script in the capsule can call Maitento without a key of its own: read a file system, run an interaction, start another capsule. The sign-in lasts as long as the capsule, and can do anything you can. Run commands you don’t trust, such as an agent’s, as a user of their own, which isn’t signed in.