Skip to main content
Images define the root filesystem a Sailbox boots from: start from a base image, optionally chain build steps, and pass the result to Sailbox.create. @sail.function additionally lets Python ship a local function into a Sailbox and run it as if it were local. See the Images guide for a task-oriented walkthrough.

Base images

arm = sail.Image.debian_arm64
amd = sail.Image.debian_amd64
# Devtools preinstalled, for use as a remote developer workstation:
dev = sail.Image.devbox_arm64
import { Image } from "@sailresearch/sdk";

const arm = Image.debian("arm64");
const amd = Image.debian("amd64");
// Devtools preinstalled, for use as a remote developer workstation:
const dev = Image.devbox("arm64");
use sail::image::{BaseImage, ImageArchitecture};
use sail::imagebuild::ImageDefinition;

let arm = ImageDefinition {
    base: Some(BaseImage::Debian),
    architecture: ImageArchitecture::Arm64,
    ..Default::default()
};
The devbox images are the Debian base plus a baked development layer: Node LTS with npm, build-essential compilers, the OS libraries editor remote servers need, the claude and codex CLIs, and common developer tools (jq, gh, fd, fzf, uv, mise, tmux, git-lfs, and more). They boot fast because the whole layer ships prebuilt, and uv/mise lazy-install further language toolchains on demand. The trade-off is that they are prebuilt only: builder methods such as apt_install and pip_install are rejected on a devbox base. Use a debian base when you need custom build steps. In Python, sail.Image.debian_arm64 and debian_amd64 (aliases debian_arm / debian_amd) pin the image to your local Python version so @sail.function can deserialize local bytecode.

The image builder

An image definition is an immutable value. Builder methods return a new definition, so you chain them and either pass the result straight to Sailbox.create (which builds it for you) or call build() to build eagerly.
image = (
    sail.Image.debian_arm64
    .apt_install("git", "curl")
    .pip_install("httpx")
    .env({"LOG_LEVEL": "info"})
    .build()
)
const spec = await Image.debian("arm64")
  .aptInstall("git", "curl")
  .pipInstall("httpx")
  .env({ LOG_LEVEL: "info" })
  .build();
use std::collections::HashMap;
use sail::image::{BaseImage, ImageArchitecture};
use sail::imagebuild::{ImageDefinition, ImageDefinitionStep};
use std::time::Duration;

let image = ImageDefinition {
    base: Some(BaseImage::Debian),
    architecture: ImageArchitecture::Arm64,
    env: HashMap::from([("LOG_LEVEL".to_string(), "info".to_string())]),
    steps: vec![
        ImageDefinitionStep::AptInstall(vec!["git".into(), "curl".into()]),
        ImageDefinitionStep::PipInstall(vec!["httpx".into()]),
    ],
    ..Default::default()
};
let spec = client
    .build_image_definition(&image, Duration::from_secs(1800))
    .await?;

apt_install

def apt_install(*packages: str) -> ImageDefinition
aptInstall(...packages: string[]): Image
ImageDefinitionStep::AptInstall(packages: Vec<String>)
Adds a step that installs Debian packages with apt. Requires at least one non-empty package name.

pip_install

def pip_install(*packages: str) -> ImageDefinition
pipInstall(...packages: string[]): Image
ImageDefinitionStep::PipInstall(packages: Vec<String>)
Adds a step that installs Python packages with pip. Requires at least one non-empty package name.

run_commands

def run_commands(*cmd: str) -> ImageDefinition
runCommand(command: string): Image
ImageDefinitionStep::RunCommand(command: String)
Adds one build step per shell command, in order. Each command must be non-empty.

add_local_file

def add_local_file(
    local_path: str | Path,
    remote_path: str,
    *,
    mode: int | None = None,
) -> ImageDefinition
addLocalFile(localPath: string, remotePath: string, options?: {
  mode?: number;
}): Image
ImageDefinitionStep::AddLocalFile {
    local_path: PathBuf,
    remote_path: String,
    mode: Option<u32>,
}
Bakes the contents of one local file into the image at remote_path. Only the file’s content hash, target path, and mode identify the image, so a one-byte change forces a rebuild.
ParameterDefaultDescription
local_pathrequiredPath to the local file.
remote_pathrequiredAbsolute POSIX destination. A trailing slash appends the local basename.
modeNonePOSIX permission bits (low 9 bits, max 0o777). Defaults to 0o644.
Raises an invalid-argument error if the source is missing, the path is invalid, or the file exceeds the 5 GiB single-file limit.

add_local_dir

def add_local_dir(
    local_path: str | Path,
    remote_path: str,
    *,
    ignore: Sequence[str] | Path | str | None = None,
) -> ImageDefinition
addLocalDir(localPath: string, remotePath: string, options?: {
  ignore?: string[];
  ignoreFile?: string;
}): Image
ImageDefinitionStep::AddLocalDir {
    local_path: PathBuf,
    remote_path: String,
    ignore: Vec<String>,
    ignore_file: Option<PathBuf>,
}
Bakes a local directory into the image at remote_path. Each regular file is hashed and uploaded; per-file modes come from the local stat. Symlinks are skipped. ignore takes gitignore-style patterns, or point at an existing ignore file (such as .gitignore) instead. remote_path must be absolute.

env

def env(env: dict[str, str]) -> ImageDefinition
env(env: Record<string, string>): Image
// ImageDefinition field:
env: HashMap<String, String>
Sets environment variables baked into the image. Requires at least one non-empty key.

build

def build(*, timeout: int = 1800) -> ImageDefinition
build(options?: { timeoutSeconds?: number }): Promise<ImageSpec>
pub async fn build_image_definition(
    &self, // Client
    def: &ImageDefinition,
    timeout: Duration,
) -> Result<ImageSpec, SailError>
Builds the image and blocks until it is ready, returning a built definition you can create sailboxes from. timeout bounds the whole pipeline (local file uploads and the build) and must be > 0. Raises an image-build error if the build fails and a timeout error if it does not finish within timeout.
You rarely need to call build() yourself: passing an unbuilt definition to Sailbox.create builds it first (bounded by image_build_timeout).

@sail.function

Python only.
@sail.function
def fn(...): ...
# or
@sail.function()
def fn(...): ...
Decorates a Python function so it can run inside a Sailbox via Sailbox.exec. The decorator returns a SailFunction; calling it locally still invokes the original function unchanged.
@sail.function
def add(x: int, y: int) -> int:
    return x + y

value = sb.exec(add, 2, 3, timeout=30)
print(value)  # 5
When you pass a SailFunction to exec, the call blocks and returns the function’s return value directly (not a ExecProcess). The SDK serializes the function plus its arguments, runs it with the image’s python3, and returns the deserialized result. Constraints:
  • Function execution is synchronous; background=True is not supported.
  • The sailbox’s python3 must match your local Python major.minor, because the serialized bytecode is version-sensitive. This is why the debian bases pin the local version.
  • Imported third-party packages are referenced by name, so they must exist in the sailbox environment.
  • Keep arguments and return values small; write large artifacts from inside the sailbox and return a small reference instead.
Raises sail.SailboxFunctionError (with the remote error_type, traceback, stdout, stderr attached) when the function raises remotely, and sail.SailboxFunctionSerializationError if the payload or result cannot be serialized or the runtime cannot be prepared. See Errors.

SailFunction

The wrapper returned by @sail.function. You normally don’t construct it directly. Calling a SailFunction locally is identical to calling the wrapped function. Async functions, async generators, and generator functions are rejected at decoration time with TypeError.
MemberDescription
funcThe wrapped callable.
__call__(*args, **kwargs)Invokes the wrapped function locally.