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

# Custom images

> Customize what a Sailbox boots with

## Just use the Sailbox

Create a Sailbox and set it up the way you would any Linux machine: copy files
in with `sail box cp`, run commands with `sail box exec`, or
[enable SSH](/sailboxes-access-control) and use `scp` and `rsync`.

When it looks right, checkpoint it. Every Sailbox you start from that
checkpoint boots with the same disk, and the same memory, already in place:

<div className="sail-prompt-shell">
  ```bash theme={null}
  sail box checkpoint <id> --name after-setup
  sail box from-checkpoint <checkpoint-id> --name worker-1
  ```
</div>

This is the fastest way to get many identical environments, and it needs no
image at all. See [Forking](/sailboxes-forking).

## Use a container image you already have

Point `Sailbox.create` at an image on a public registry. Sail pulls it and
layers what a Sailbox needs on top.

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

  image = sail.Image.from_registry("myorg/my-custom-image:latest")
  sb = sail.Sailbox.create(app=app, name="custom", image=image)
  ```

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

  const image = Image.fromRegistry("myorg/my-custom-image:latest");
  const sb = await Sailbox.create({ app, name: "custom", image });
  ```

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

  let image = ImageDefinition {
      oci_ref: Some("myorg/my-custom-image:latest".to_string()),
      ..Default::default()
  };
  ```
</CodeGroup>

Write the reference as you would for `docker pull`. The image must be
Debian- or Ubuntu-based and publicly pullable from `docker.io`, `ghcr.io`,
`public.ecr.aws`, or `quay.io`. Private registries are not supported.

### How Sail treats your image

* **`ENV`, `WORKDIR`, and `USER` become the defaults** for every command you
  run in the Sailbox. Commands run as the image's `USER` when it sets one and
  as root otherwise. Pass `user="0:0"` on a call to run as root anyway.
* **`ENTRYPOINT` and `CMD` are not run.** A Sailbox manages its own
  processes; your commands say what to execute.
* **The image keeps its own `python3`.** Sail never installs another Python
  over it, because a pinned interpreter would shadow the one the image was
  built around.
* **A few paths are Sail's.** The build replaces `/init` and some Sail-owned
  files under `/usr/local/bin`, and writes configuration under `/etc/sailbox`
  and at `/etc/profile.d/sailbox-env.sh`. Everything else is left alone.
* **The Sailbox runs on the architecture the image was built for.** An image
  published for both amd64 and arm64 runs on amd64; pass `architecture` to
  require one.

## Build one from a Dockerfile

Already have a Dockerfile? Sail builds it for you.

<CodeGroup>
  ```python Python theme={null}
  image = sail.Image.from_dockerfile("./Dockerfile", context_dir=".")
  ```

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

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

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

`context_dir` is where `COPY` and `ADD` read from, with `.dockerignore`
honored. Every `FROM` and `COPY --from` must name a public image on one of
the registries above, and the result must be Debian- or Ubuntu-based. When a
step fails, the error includes that step's output.

<Accordion title="Dockerfile support details">
  * Pass a path or the Dockerfile text itself (`contents=` in Python,
    `{ contents }` in TypeScript, `DockerfileInput::Contents` in Rust).
  * The build runs for amd64 unless you pass `architecture`. `build_args`
    fill `ARG` instructions like `--build-arg`. Names starting with
    `BUILDKIT_` and Docker's proxy variables (`HTTP_PROXY` and friends) are
    rejected; a `RUN` step can set a proxy for itself.
  * A `Dockerfile.dockerignore` next to the Dockerfile replaces the context's
    `.dockerignore`, and `ignore` patterns you pass win over both. Python
    snapshots the context when you call `from_dockerfile`; TypeScript and
    Rust do it when the image is built. Edits after that point do not reach
    the build.
  * The context keeps file modes, empty directories, and symlinks. Hard links
    arrive as separate files. Setuid, setgid, and sticky bits, named pipes,
    device nodes, and mode `000` entries are rejected; sockets are skipped.
  * Up to 25 different images per Dockerfile across `FROM` and `COPY --from`.
  * Multi-stage builds, `tmpfs` mounts, and `bind` mounts from the context or
    another stage work. `RUN --mount` of type `cache`, `secret`, or `ssh`, a
    `bind` mount whose `from` names another image, mount options that are
    variable references, and `ONBUILD` (in your file or a base image) are
    rejected.
  * A `# syntax=` line may declare `docker/dockerfile:1` or a release from
    1.4 through 1.22.0. Anything else is rejected. The line does not change
    how the file is built.
</Accordion>

## Build one in code

No Dockerfile? Start from Sail's Debian base and chain the steps you need.
Each step returns a new definition, so one base can serve several variants.

<CodeGroup>
  ```python Python theme={null}
  image = (
      sail.Image.debian_amd64
      .apt_install("git", "curl")
      .pip_install("requests")
      .add_local_dir("./app", "/opt/app", ignore=["*.pyc", "__pycache__/"])
      .add_local_file("./config.json", "/etc/app/config.json", mode=0o600)
      .run_commands("python3 -m pip show requests >/tmp/requests.txt")
      .env({"APP_ENV": "production"})
  )
  ```

  ```typescript TypeScript theme={null}
  const image = Image.debian("amd64")
    .aptInstall("git", "curl")
    .pipInstall("requests")
    .addLocalDir("./app", "/opt/app", { ignore: ["*.pyc", "__pycache__/"] })
    .addLocalFile("./config.json", "/etc/app/config.json", { mode: 0o600 })
    .runCommand("python3 -m pip show requests >/tmp/requests.txt")
    .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::Amd64,
      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,
          },
          ImageDefinitionStep::AddLocalFile {
              local_path: "./config.json".into(),
              remote_path: "/etc/app/config.json".into(),
              mode: Some(0o600),
          },
          ImageDefinitionStep::RunCommand(
              "python3 -m pip show requests >/tmp/requests.txt".to_string(),
          ),
      ],
      ..Default::default()
  };
  ```
</CodeGroup>

| Step                               | What it does                                                                                                                                        |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `apt_install(...)`                 | Installs Debian packages.                                                                                                                           |
| `pip_install(...)`                 | Installs Python packages into the image's `python3`.                                                                                                |
| `add_local_file(src, dst, mode=)`  | Copies one file. `mode` sets its permissions.                                                                                                       |
| `add_local_dir(src, dst, ignore=)` | Copies a directory tree, keeping file modes and skipping symlinks. `ignore` takes gitignore-style patterns or the path of a file like `.gitignore`. |
| `run_commands(...)`                | Runs a shell command once, during the build.                                                                                                        |
| `env({...})`                       | Sets environment variables for every command run from the image.                                                                                    |

The same steps chain onto a registry image or a Dockerfile image too. Remote
paths must be absolute and cannot contain a space, `$`, `"`, or `\`. Variable
names must start with a letter or `_` and contain only letters, digits, and
`_`.

<Note>
  `sail.Image.debian_amd64` and `debian_arm64` also install a Python matching
  your local interpreter, so that
  [`@sail.function`](/sailbox-sdk-images#sail-function) can run your Python
  functions inside the Sailbox. Use `sail.Image.debian("amd64",
      install_python=False)` to keep the base's stock `python3`.
</Note>

## Building and caching

Pass an image definition to `Sailbox.create` and Sail uploads any local files,
builds the image if it has not been built before, and starts the Sailbox from
it. `image_build_timeout` (`imageBuildTimeoutSeconds` in TypeScript) bounds
the build, retries included. In Rust the timeout is the duration passed to
`build_image_definition`. To build ahead of time instead, call `build` on the
definition and pass the result to `Sailbox.create`. In Rust,
`build_image_definition` is the ahead-of-time build.

* **Builds are cached by content, per organization.** The same base, steps,
  variables, and file contents reuse the existing image.
* **Tags are pinned, per organization.** The first build from a tag such as
  `python:3.13`, or from a `FROM` line, pins the version the tag pointed at,
  and later builds keep it even after the tag moves upstream. Pass
  `force_build` (`forceBuild` in TypeScript, `BuildMode::ForceBuild` in Rust)
  to look the tag up again. That moves the pin for the whole organization;
  Sailboxes that already exist keep the version they started on. A digest
  (`name@sha256:...`) never moves.
* **The first build of a large image downloads all of it.** Later builds
  usually reuse its layers.

<Accordion title="Your image may boot when you are not looking">
  After a build, and periodically while an image is in use, Sail boots it
  outside any Sailbox to capture a start snapshot. Sailboxes created from the
  image resume from that snapshot instead of cold-booting, which keeps starts
  fast at every size.

  This makes two things part of the image contract:

  * **Boot-time initialization runs during every hidden boot.** systemd units
    and init scripts must be safe to run repeatedly, outside any Sailbox. Your
    entrypoint and the commands you run in a Sailbox never run during a hidden
    boot.
  * **State written at boot is shared.** Whatever boot leaves on disk or in
    memory is in the snapshot every Sailbox resumes from. Generate per-instance
    identity (machine IDs, nonces, cached credentials) at runtime, for example
    in your application's entrypoint, not at boot. Environment variables,
    networking, and Sail-managed credentials are applied per Sailbox after the
    resume, so they behave the same either way.
</Accordion>
