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

# Images & Functions

> Base images, the custom-image builder, and running Python functions in a Sailbox

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`](/sailbox-sdk#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](/sailboxes-images) for a task-oriented
walkthrough.

## Base images

<CodeGroup>
  ```python Python theme={null}
  arm = sail.Image.debian_arm64
  amd = sail.Image.debian_amd64
  # Devtools preinstalled, for use as a remote developer workstation:
  dev = sail.Image.devbox("arm64")
  ```

  ```typescript TypeScript theme={null}
  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");
  ```

  ```rust Rust theme={null}
  use sail::{BaseImage, ImageArchitecture};
  use sail::imagebuild::ImageDefinition;

  let arm = ImageDefinition {
      base: Some(BaseImage::Debian),
      architecture: ImageArchitecture::Arm64,
      ..Default::default()
  };
  ```
</CodeGroup>

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, Docker with the `docker compose`
and `docker buildx` plugins, and common developer tools (`jq`, `gh`, `fd`,
`fzf`, `uv`, `mise`, `tmux`, `git-lfs`, and more). Devbox images 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.

The Docker daemon starts automatically when a devbox Sailbox boots and keeps
running across sleeps. Right after a fresh boot, the daemon can take a few
seconds to accept commands. If it ever stops, start `dockerd` again as root.
If it complains about a stale pid file, delete `/var/run/docker.pid` and
retry.

Devbox images also have a working clipboard. During `sail box shell`,
Ctrl+V puts your local clipboard on the guest's clipboard. Pasting a
screenshot into `claude` or `codex` works exactly as it does on your own
machine, and text copied inside the guest is copied back to your local
clipboard.

In Python, `sail.Image.debian_arm64` and `debian_amd64` are shorthand for
`sail.Image.debian("arm64")` and `debian("amd64")`, and pin the image to your
local Python version so `@sail.function` can deserialize local bytecode. Pass
`install_python=False`, as in `sail.Image.debian("arm64", install_python=False)`,
to keep the base's stock `python3` instead.

### from\_registry

Use your own image as the root filesystem. Sail pulls it and layers the
Sailbox runtime on top, so every builder method works the same as on a
Debian base.

<CodeGroup>
  ```python Python theme={null}
  image = sail.Image.from_registry("python:3.13")
  ```

  ```typescript TypeScript theme={null}
  const image = Image.fromRegistry("python:3.13");
  ```

  ```rust Rust theme={null}
  let image = ImageDefinition {
      oci_ref: Some("python:3.13".to_string()),
      ..Default::default()
  };
  ```
</CodeGroup>

Reference a Debian- or Ubuntu-based image on a supported public registry
(`docker.io`, `ghcr.io`, `public.ecr.aws`, or `quay.io`), written as you
would for `docker pull`: `python:3.13` means `docker.io/library/python:3.13`,
`acme/tool` means `docker.io/acme/tool`, and the other registries are named
in full, as in `ghcr.io/acme/tool`. You can pass a tag, a digest (`name@sha256:...`), or just the name,
which means the `latest` tag. A tag is pinned for your organization once an
image has been built from it: later builds keep getting that version, even
if the tag moves upstream. Use `force_build`
(`forceBuild` in TypeScript, `BuildMode::ForceBuild` in Rust) to look the
tag up again and move the pin for your whole organization. If forced
builds of the same tag overlap, the last-requested one that succeeds
decides what the tag means, no matter which build finishes first. A
digest names
exactly one image, so it never moves. Your Sailbox runs on the CPU architecture the image was built for;
an image published for both amd64 and arm64 runs on amd64. Pass
`architecture` to require one, and the build fails if the image was not
built for it. Its environment variables, working directory, and `USER`
become the defaults for commands you run; its `ENTRYPOINT` and `CMD` are
not run. See
[Bring your own base image](/sailboxes-images#bring-your-own-base-image)
for the full requirements.

### from\_dockerfile

Build your own Dockerfile into the image. Sail builds it and layers the
Sailbox runtime on top, so every builder method composes on the result.

<CodeGroup>
  ```python Python theme={null}
  from pathlib import Path

  env_dir = Path("./envs/task1")
  image = sail.Image.from_dockerfile(env_dir / "Dockerfile", context_dir=env_dir)
  ```

  ```typescript TypeScript theme={null}
  const image = Image.fromDockerfile("./envs/task1/Dockerfile", {
    contextDir: "./envs/task1",
  });
  ```

  ```rust Rust theme={null}
  use std::collections::HashMap;
  use sail::imagebuild::{DockerfileInput, DockerfileSource, ImageDefinition};

  let image = ImageDefinition {
      dockerfile: Some(DockerfileSource {
          dockerfile: DockerfileInput::Path("./envs/task1/Dockerfile".into()),
          context_dir: Some("./envs/task1".into()),
          build_args: HashMap::new(),
          ignore: Vec::new(),
      }),
      ..Default::default()
  };
  ```
</CodeGroup>

Pass the path to a Dockerfile, or its literal text with `contents=`
(`{ contents }` in TypeScript, `DockerfileInput::Contents` in Rust).
`context_dir` is the directory `COPY` and `ADD` read from, with
`.dockerignore` honored (a `.dockerignore` file named after your
Dockerfile, for example `Dockerfile.dockerignore`, is used instead when
present, as with Docker) and `ignore` patterns applied on top. Every
image a `FROM` or `COPY --from` names must live on a supported public
registry, and a short name works as you would expect: `FROM python:3.12`
means `docker.io/library/python:3.12`. The image the Dockerfile produces
must be Debian- or Ubuntu-based. The build runs for amd64 unless you
pass `architecture`, and `build_args` values fill the Dockerfile's `ARG`
instructions. Tags a `FROM` or `COPY --from` names are pinned on your
organization's first use and reused after that; `force_build`
(`forceBuild` in TypeScript, `BuildMode::ForceBuild` in Rust) looks them
up again. See
[Build from a Dockerfile](/sailboxes-images#build-from-a-dockerfile) for
the full behavior.

## 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.

<CodeGroup>
  ```python Python theme={null}
  image = (
      sail.Image.debian_arm64
      .apt_install("git", "curl")
      .pip_install("httpx")
      .env({"LOG_LEVEL": "info"})
      .build()
  )
  ```

  ```typescript TypeScript theme={null}
  const spec = await Image.debian("arm64")
    .aptInstall("git", "curl")
    .pipInstall("httpx")
    .env({ LOG_LEVEL: "info" })
    .build();
  ```

  ```rust Rust theme={null}
  use std::collections::HashMap;
  use sail::{BaseImage, ImageArchitecture};
  use sail::imagebuild::{BuildMode, 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), BuildMode::ReuseExisting)
      .await?;
  ```
</CodeGroup>

### apt\_install

<CodeGroup>
  ```python Python theme={null}
  def apt_install(*packages: str) -> ImageDefinition
  ```

  ```typescript TypeScript theme={null}
  aptInstall(...packages: string[]): Image
  ```

  ```rust Rust theme={null}
  ImageDefinitionStep::AptInstall(packages: Vec<String>)
  ```
</CodeGroup>

Adds a step that installs Debian packages with `apt`. Requires at least one
non-empty package name.

### pip\_install

<CodeGroup>
  ```python Python theme={null}
  def pip_install(*packages: str) -> ImageDefinition
  ```

  ```typescript TypeScript theme={null}
  pipInstall(...packages: string[]): Image
  ```

  ```rust Rust theme={null}
  ImageDefinitionStep::PipInstall(packages: Vec<String>)
  ```
</CodeGroup>

Adds a step that installs Python packages with `pip`. Requires at least one
non-empty package name.

### run\_commands

<CodeGroup>
  ```python Python theme={null}
  def run_commands(*cmd: str) -> ImageDefinition
  ```

  ```typescript TypeScript theme={null}
  runCommand(command: string): Image
  ```

  ```rust Rust theme={null}
  ImageDefinitionStep::RunCommand(command: String)
  ```
</CodeGroup>

Adds one build step per shell command, in order. Each command must be
non-empty.

### add\_local\_file

<CodeGroup>
  ```python Python theme={null}
  def add_local_file(
      local_path: str | Path,
      remote_path: str,
      *,
      mode: int | None = None,
  ) -> ImageDefinition
  ```

  ```typescript TypeScript theme={null}
  addLocalFile(localPath: string, remotePath: string, options?: {
    mode?: number;
  }): Image
  ```

  ```rust Rust theme={null}
  ImageDefinitionStep::AddLocalFile {
      local_path: PathBuf,
      remote_path: String,
      mode: Option<u32>,
  }
  ```
</CodeGroup>

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.

| Parameter     | Default  | Description                                                              |
| ------------- | -------- | ------------------------------------------------------------------------ |
| `local_path`  | required | Path to the local file.                                                  |
| `remote_path` | required | Absolute POSIX destination. A trailing slash appends the local basename. |
| `mode`        | `None`   | POSIX 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

<CodeGroup>
  ```python Python theme={null}
  def add_local_dir(
      local_path: str | Path,
      remote_path: str,
      *,
      ignore: Sequence[str] | Path | str | None = None,
  ) -> ImageDefinition
  ```

  ```typescript TypeScript theme={null}
  addLocalDir(localPath: string, remotePath: string, options?: {
    ignore?: string[];
    ignoreFile?: string;
  }): Image
  ```

  ```rust Rust theme={null}
  ImageDefinitionStep::AddLocalDir {
      local_path: PathBuf,
      remote_path: String,
      ignore: Vec<String>,
      ignore_file: Option<PathBuf>,
  }
  ```
</CodeGroup>

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

<CodeGroup>
  ```python Python theme={null}
  def env(env: dict[str, str]) -> ImageDefinition
  ```

  ```typescript TypeScript theme={null}
  env(env: Record<string, string>): Image
  ```

  ```rust Rust theme={null}
  // ImageDefinition field:
  env: HashMap<String, String>
  ```
</CodeGroup>

Sets environment variables baked into the image. Requires at least one
non-empty key.

### build

<CodeGroup>
  ```python Python theme={null}
  def build(*, timeout: int = 1800, force_build: bool = False) -> ImageDefinition
  ```

  ```typescript TypeScript theme={null}
  build(options?: {
    timeoutSeconds?: number;
    forceBuild?: boolean;
  }): Promise<ImageSpec>
  ```

  ```rust Rust theme={null}
  pub async fn build_image_definition(
      &self, // Client
      def: &ImageDefinition,
      timeout: Duration,
      mode: BuildMode,
  ) -> Result<ImageSpec, SailError>
  ```
</CodeGroup>

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`.

Everything you create from the result needs no further build. By default,
Sail may reuse an existing ready build for the definition
(`BuildMode::ReuseExisting` in Rust). Use `force_build=True`,
`forceBuild: true`, or `BuildMode::ForceBuild` to build it again: new
Sailboxes use the fresh image once it is ready, Sailboxes that already
exist keep the filesystem they were created with, and a forced build that
fails changes nothing.

For an image imported with [`from_registry`](#from_registry) through a tag,
a forced build also asks the registry what the tag points at now and builds
that version. The tag then means that version for your whole organization,
while the result of an earlier build keeps its pinned version. For an image
built with [`from_dockerfile`](#from_dockerfile), a forced build looks up
the tags its `FROM` and `COPY --from` instructions name and moves those
pins for your whole organization, while the result of an earlier build
keeps the versions its build used. If forced builds overlap, the last-requested
one that succeeds decides which image new Sailboxes use and, for a tag,
what the tag means.

Raises an image-build error if the build fails and a timeout error if it does
not finish within `timeout`.

<Note>
  You rarely need to call `build()` yourself: passing an unbuilt definition to
  [`Sailbox.create`](/sailbox-sdk#sailbox-create) builds it first (bounded by
  `image_build_timeout`).
</Note>

***

<h2 id="sail-function">
  @sail.function
</h2>

Python only.

```python theme={null}
@sail.function
def fn(...): ...
# or
@sail.function()
def fn(...): ...
```

Decorates a Python function so it can run inside a Sailbox via
[`Sailbox.exec`](/sailbox-sdk#exec). The decorator returns a
[`SailFunction`](#sailfunction); calling it locally still invokes the original
function unchanged.

```python theme={null}
@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. The function's complete
  encoded response (its serialized return value, captured stdout and stderr,
  and any error details, as encoded on the wire) must fit the exec's
  `output_buffer_bytes` (1 MiB by default, up to 64 MiB); a larger response
  raises `sail.SailboxFunctionSerializationError`. A second call with the same `idempotency_key` while the function runs takes over its output, and the earlier call may then fail to decode its result. `output_mode` must stay `auto`
  for a function.

**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](/sailbox-sdk-errors).

<h3 id="sailfunction">
  SailFunction
</h3>

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`.

| Member                      | Description                           |
| --------------------------- | ------------------------------------- |
| `func`                      | The wrapped callable.                 |
| `__call__(*args, **kwargs)` | Invokes the wrapped function locally. |
