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

> Build Sailbox images with packages, filesystems, local files, commands, and environment variables

Sailbox images define the root filesystem used to start a VM. Start from a
Debian base image, then chain build steps to install dependencies, copy local
files, run setup commands, and set environment variables.

<CodeGroup>
  ```python Python theme={null}
  import sail

  image = (
      sail.Image.debian_arm64
      .apt_install("git", "curl")
      .pip_install("requests")
      .add_local_dir("./app", "/opt/app", ignore=["*.pyc", "__pycache__/"])
      .env({"APP_ENV": "production"})
  )
  ```

  ```typescript TypeScript theme={null}
  import { Image } from "@sailresearch/sdk";

  const image = Image.debian("arm64")
    .aptInstall("git", "curl")
    .pipInstall("requests")
    .addLocalDir("./app", "/opt/app", { ignore: ["*.pyc", "__pycache__/"] })
    .env({ APP_ENV: "production" });
  ```

  ```rust Rust theme={null}
  use std::collections::HashMap;

  use sail::{BaseImage, ImageArchitecture};
  use sail::imagebuild::{ImageDefinition, ImageDefinitionStep};

  let image = ImageDefinition {
      base: Some(BaseImage::Debian),
      architecture: ImageArchitecture::Arm64,
      env: HashMap::from([("APP_ENV".to_string(), "production".to_string())]),
      steps: vec![
          ImageDefinitionStep::AptInstall(vec!["git".into(), "curl".into()]),
          ImageDefinitionStep::PipInstall(vec!["requests".into()]),
          ImageDefinitionStep::AddLocalDir {
              local_path: "./app".into(),
              remote_path: "/opt/app".into(),
              ignore: vec!["*.pyc".into(), "__pycache__/".into()],
              ignore_file: None,
          },
      ],
      ..Default::default()
  };
  ```
</CodeGroup>

Image definitions are immutable values: each builder step gives you a new
definition, so you can safely reuse a base image across multiple variants.

## Base images

Use a Debian base image for the target architecture:

<CodeGroup>
  ```python Python theme={null}
  arm_image = sail.Image.debian_arm64
  amd_image = sail.Image.debian_amd64
  ```

  ```typescript TypeScript theme={null}
  const armImage = Image.debian("arm64");
  const amdImage = Image.debian("amd64");
  ```

  ```rust Rust theme={null}
  let arm_image = ImageDefinition {
      base: Some(BaseImage::Debian),
      architecture: ImageArchitecture::Arm64,
      ..Default::default()
  };
  let amd_image = ImageDefinition {
      base: Some(BaseImage::Debian),
      architecture: ImageArchitecture::Amd64,
      ..Default::default()
  };
  ```
</CodeGroup>

In Python, `sail.Image.debian_arm64` and `debian_amd64` (aliases
`debian_arm` / `debian_amd`) pin the image to your local Python version for
`@sail.function`.

## Install Python packages

Install Python dependencies at build time:

<CodeGroup>
  ```python Python theme={null}
  image = sail.Image.debian_arm64.pip_install(
      "requests",
      "beautifulsoup4",
  )
  ```

  ```typescript TypeScript theme={null}
  const image = Image.debian("arm64").pipInstall("requests", "beautifulsoup4");
  ```

  ```rust Rust theme={null}
  let image = ImageDefinition {
      base: Some(BaseImage::Debian),
      architecture: ImageArchitecture::Arm64,
      steps: vec![ImageDefinitionStep::PipInstall(vec![
          "requests".into(),
          "beautifulsoup4".into(),
      ])],
      ..Default::default()
  };
  ```
</CodeGroup>

Package installation happens at image build time, before any Sailbox starts.
This is usually faster and more reproducible than installing packages in every
new VM with `exec()`.

## Add local files

Copy a single local file into the image:

<CodeGroup>
  ```python Python theme={null}
  image = sail.Image.debian_arm64.add_local_file(
      "./config.json",
      "/etc/demo/config.json",
      mode=0o600,
  )
  ```

  ```typescript TypeScript theme={null}
  const image = Image.debian("arm64").addLocalFile(
    "./config.json",
    "/etc/demo/config.json",
    { mode: 0o600 },
  );
  ```

  ```rust Rust theme={null}
  let image = ImageDefinition {
      base: Some(BaseImage::Debian),
      architecture: ImageArchitecture::Arm64,
      steps: vec![ImageDefinitionStep::AddLocalFile {
          local_path: "./config.json".into(),
          remote_path: "/etc/demo/config.json".into(),
          mode: Some(0o600),
      }],
      ..Default::default()
  };
  ```
</CodeGroup>

Or copy a directory tree:

<CodeGroup>
  ```python Python theme={null}
  image = sail.Image.debian_arm64.add_local_dir(
      "./app",
      "/opt/demo-app",
      ignore=["*.pyc", "__pycache__/"],
  )
  ```

  ```typescript TypeScript theme={null}
  const image = Image.debian("arm64").addLocalDir("./app", "/opt/demo-app", {
    ignore: ["*.pyc", "__pycache__/"],
  });
  ```

  ```rust Rust theme={null}
  let image = ImageDefinition {
      base: Some(BaseImage::Debian),
      architecture: ImageArchitecture::Arm64,
      steps: vec![ImageDefinitionStep::AddLocalDir {
          local_path: "./app".into(),
          remote_path: "/opt/demo-app".into(),
          ignore: vec!["*.pyc".into(), "__pycache__/".into()],
          ignore_file: None,
      }],
      ..Default::default()
  };
  ```
</CodeGroup>

Remote paths must be absolute POSIX paths, and cannot contain a space, `$`,
`"`, or `\`. File names inside an uploaded directory cannot contain `$`, `"`,
or `\` either. Directory uploads preserve file
modes from the local filesystem and skip symlinks. `ignore` accepts
gitignore-style patterns, or point at an existing ignore file (such as
`.gitignore`) instead.

Use image files for source code, static assets, and configuration that should
exist before boot. Use [Filesystem](/sailboxes-filesystem) for runtime inputs,
outputs, logs, and data that changes per Sailbox.

## Install system packages

Install Debian packages with apt:

<CodeGroup>
  ```python Python theme={null}
  image = sail.Image.debian_arm64.apt_install(
      "git",
      "curl",
      "openssh-server",
  )
  ```

  ```typescript TypeScript theme={null}
  const image = Image.debian("arm64").aptInstall("git", "curl", "openssh-server");
  ```

  ```rust Rust theme={null}
  let image = ImageDefinition {
      base: Some(BaseImage::Debian),
      architecture: ImageArchitecture::Arm64,
      steps: vec![ImageDefinitionStep::AptInstall(vec![
          "git".into(),
          "curl".into(),
          "openssh-server".into(),
      ])],
      ..Default::default()
  };
  ```
</CodeGroup>

## Run shell commands

Run shell commands during the image build:

<CodeGroup>
  ```python Python theme={null}
  image = (
      sail.Image.debian_arm64
      .apt_install("python3")
      .pip_install("requests")
      .run_commands("python3 -m pip show requests >/tmp/requests.txt")
  )
  ```

  ```typescript TypeScript theme={null}
  const image = Image.debian("arm64")
    .aptInstall("python3")
    .pipInstall("requests")
    .runCommand("python3 -m pip show requests >/tmp/requests.txt");
  ```

  ```rust Rust theme={null}
  let image = ImageDefinition {
      base: Some(BaseImage::Debian),
      architecture: ImageArchitecture::Arm64,
      steps: vec![
          ImageDefinitionStep::AptInstall(vec!["python3".into()]),
          ImageDefinitionStep::PipInstall(vec!["requests".into()]),
          ImageDefinitionStep::RunCommand(
              "python3 -m pip show requests >/tmp/requests.txt".to_string(),
          ),
      ],
      ..Default::default()
  };
  ```
</CodeGroup>

Build commands run once while the image is prepared. They do not run each time a
Sailbox starts.

## Set environment variables

Bake environment variables into Sailboxes created from the image:

<CodeGroup>
  ```python Python theme={null}
  image = sail.Image.debian_arm64.env(
      {
          "APP_ENV": "demo",
          "LOG_LEVEL": "info",
      }
  )
  ```

  ```typescript TypeScript theme={null}
  const image = Image.debian("arm64").env({
    APP_ENV: "demo",
    LOG_LEVEL: "info",
  });
  ```

  ```rust Rust theme={null}
  let image = ImageDefinition {
      base: Some(BaseImage::Debian),
      architecture: ImageArchitecture::Arm64,
      env: HashMap::from([
          ("APP_ENV".to_string(), "demo".to_string()),
          ("LOG_LEVEL".to_string(), "info".to_string()),
      ]),
      ..Default::default()
  };
  ```
</CodeGroup>

A variable name must start with a letter or `_` and contain only letters,
numbers, and `_`. Anything else is rejected when the image is built.

## Create a Sailbox from an image

Pass the image definition to `Sailbox.create()`:

<CodeGroup>
  ```python Python theme={null}
  app = sail.App.find(name="custom-image-demo", mint_if_missing=True)

  image = (
      sail.Image.debian_arm64
      .apt_install("git", "curl")
      .pip_install("requests")
      .add_local_dir("./app", "/opt/app", ignore=["*.pyc", "__pycache__/"])
      .env({"APP_ENV": "custom-image-demo"})
  )

  sb = sail.Sailbox.create(
      app=app,
      image=image,
      name="custom-image-demo",
      image_build_timeout=1800,
  )
  ```

  ```typescript TypeScript theme={null}
  const app = await App.find("custom-image-demo", { mintIfMissing: true });

  const image = Image.debian("arm64")
    .aptInstall("git", "curl")
    .pipInstall("requests")
    .addLocalDir("./app", "/opt/app", { ignore: ["*.pyc", "__pycache__/"] })
    .env({ APP_ENV: "custom-image-demo" });

  const sb = await Sailbox.create({
    app,
    name: "custom-image-demo",
    image,
    imageBuildTimeoutSeconds: 1800,
  });
  ```

  ```rust Rust theme={null}
  let image = ImageDefinition {
      base: Some(BaseImage::Debian),
      architecture: ImageArchitecture::Arm64,
      env: HashMap::from([("APP_ENV".to_string(), "custom-image-demo".to_string())]),
      steps: vec![
          ImageDefinitionStep::AptInstall(vec!["git".into(), "curl".into()]),
          ImageDefinitionStep::PipInstall(vec!["requests".into()]),
          ImageDefinitionStep::AddLocalDir {
              local_path: "./app".into(),
              remote_path: "/opt/app".into(),
              ignore: vec!["*.pyc".into(), "__pycache__/".into()],
              ignore_file: None,
          },
      ],
      ..Default::default()
  };

  let spec = client
      .build_image_definition(&image, Duration::from_secs(1800))
      .await?;

  let app = client.find_app("custom-image-demo", /* mint_if_missing */ true).await?;
  let handle = client
      .create_sailbox(
          &CreateSailboxRequest {
              app_id: app.id,
              name: "custom-image-demo".to_string(),
              image: spec,
              ..Default::default()
          },
          /* timeout */ None,
      )
      .await?;
  ```
</CodeGroup>

In Python and TypeScript, `Sailbox.create()` uploads any local files, builds
the image if it has not already been built, then starts the VM from that image.
In Rust, `build_image_definition` runs that same upload-and-build pipeline and
returns the built spec to create from.

## Build an image ahead of time

Build the image before creating a Sailbox:

<CodeGroup>
  ```python Python theme={null}
  image = (
      sail.Image.debian_arm64
      .apt_install("git")
      .pip_install("requests")
      .build(timeout=1800)
  )

  sb = sail.Sailbox.create(
      app=app,
      image=image,
      name="prebuilt-image-demo",
  )
  ```

  ```typescript TypeScript theme={null}
  const spec = await Image.debian("arm64")
    .aptInstall("git")
    .pipInstall("requests")
    .build({ timeoutSeconds: 1800 });

  const sb = await Sailbox.create({
    app,
    name: "prebuilt-image-demo",
    image: spec,
  });
  ```

  ```rust Rust theme={null}
  let image = ImageDefinition {
      base: Some(BaseImage::Debian),
      architecture: ImageArchitecture::Arm64,
      steps: vec![
          ImageDefinitionStep::AptInstall(vec!["git".into()]),
          ImageDefinitionStep::PipInstall(vec!["requests".into()]),
      ],
      ..Default::default()
  };

  let spec = client
      .build_image_definition(&image, Duration::from_secs(1800))
      .await?;

  let handle = client
      .create_sailbox(
          &CreateSailboxRequest {
              app_id: app_id.clone(),
              name: "prebuilt-image-demo".to_string(),
              image: spec,
              ..Default::default()
          },
          /* timeout */ None,
      )
      .await?;
  ```
</CodeGroup>

## Hidden boots and start snapshots

Sail may boot your image outside of any Sailbox you create, first as the
final stage of the build pipeline and again periodically while the image is
in active use, to capture and refresh a start snapshot. Sailboxes created
from the image resume from that snapshot instead of cold-booting, which
makes starts fast at any size. Refreshing matters because platform updates
invalidate existing snapshots; re-capturing keeps an image's creates fast
for as long as it is being used.

A new custom image becomes ready only after Sail captures start snapshots for
the S, M, and L sizes. This increases the total image build time. It removes
the cold boot from the first Sailbox that uses the image. Built-in images use
snapshots that Sail prepares when Sailbox capacity starts.

This is part of the image contract:

* **Your image may boot at times you don't control.** Boot-time
  initialization (systemd units, init scripts, services configured to start
  on boot) runs during every hidden boot, at build time and during periodic
  refreshes while the image is in use. Write boot-time jobs to be
  safe to run repeatedly, at any time, outside any Sailbox.
* **State generated during a hidden boot may be shared.** Anything your
  boot process writes to disk or leaves in memory becomes part of the start
  snapshot that every Sailbox created from this image resumes from. Do not
  generate per-instance identity (machine IDs, cryptographic nonces, cached
  credentials) during boot and expect it to be unique per Sailbox.
* **Per-Sailbox identity is injected at create time.** Environment variables,
  networking, and Sail-managed credentials are applied when each Sailbox is
  created, after the snapshot resumes, so runtime configuration behaves the
  same whether or not a start snapshot was used.
* Snapshot refreshes after platform updates are best-effort. If a refreshed
  snapshot is not available, the next Sailbox can use a cold boot.

Generate anything that must be unique per Sailbox at runtime (for example in
your application entrypoint), not during image boot.

## Image caching

Sail caches image builds per organization by content. If the base image, build steps,
environment variables, and uploaded file contents are unchanged, later Sailboxes
can reuse the existing image instead of rebuilding it. A change to any build
step or local file content creates a new image.
