Skip to main content

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 and use scp and rsync. When it looks right, checkpoint it. Every Sailbox you start from that checkpoint boots with the same disk and memory already in place:
This is the fastest way to get many identical environments, and it does not need an image at all. See Forking.

Bring your own base image

Point Sailbox.create at an image on a public registry. Sail pulls it and layers what a Sailbox needs on top.
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 an image from a Dockerfile

Already have a Dockerfile? Sail builds it for you.
context_dir is where COPY and ADD read from, with .dockerignore honored. Every FROM and COPY --from must reference 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. When Sail stops a build for running too long, the error lists the slowest finished steps and the output of the steps that were still running.
  • 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 take precedence 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 are not included in the build.
  • The context preserves file modes, empty directories, and symlinks. Hard links become 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 work, and target names the stage to stop at, like docker build --target. 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.

Build an image from a Compose project

Have a Docker Compose project? Sail builds it into a Sailbox image that has Docker, the project’s service images, and a copy of the project. Create a Sailbox from that image and call compose_up to start the project.
Pass the directory you would run docker compose in. Each service needs a build inside that directory or a public image on one of the registries above. The image contains the project at /root/<its directory name>, and compose_up runs docker compose up there. After you change the project, call from_compose again to build a new image.
  • Compose fills a ${VAR} in a compose file from the shell it runs in, then from the project’s .env. The build runs on Sail, so env stands in for your shell: a variable in env is used ahead of the same one in .env, and a project whose .env defines everything it needs takes no env at all.
  • The build reads only the fields that choose and build the service images: a service’s image, build, platform, and extends, and the project’s name. A ${VAR} in those fields must have a value in .env or in env. A ${VAR} anywhere else, such as in a service’s environment or ports, is filled when the project starts, from .env and from the env passed to compose_up.
  • env can hold every variable the project runs with, secrets included. Only the variables the build reads are sent to Sail, with those the project’s .env references. The rest stay on your machine, and the image does not hold them.
  • Only the project directory is built and copied. A COMPOSE_FILE entry that is absolute or leads outside it is rejected. A bind mount whose source is outside it mounts an empty directory unless you upload that source first with upload_dir. A ../data source resolves to /root/data.
  • files lists the compose files to build from, in place of the ones Compose finds in the directory, and project_name names the project. A listed file may be outside the project directory. compose_up does not know which files or name the image was built with, so pass it the same name and the same files in the same order, as paths on the Sailbox. A listed file inside the project directory is in the image’s copy at the same relative place. One outside it is not in the image, so write it to the Sailbox before compose_up.
  • The copy leaves out what the project root’s .dockerignore excludes, except the compose files, the .env files Compose reads, and every .dockerignore, which are always kept. A service’s Dockerfile and the files it copies must not be excluded. Each service’s own .dockerignore applies to its build as usual.
  • Python reads the project when you call from_compose. TypeScript and Rust read it when the image is built. Edits made after that are not included in the build.
  • The image builds for amd64 unless you pass architecture. A project may merge up to 16 compose files and have up to 32 services. It may reference up to 64 different registry images, and its compose files may total 1 MiB. The project copy and the service images are stored on the Sailbox image’s disk, so the whole image must fit in the largest Sailbox disk, 1024 GiB.
  • Services under profiles are built too. A build may set context, dockerfile, dockerfile_inline, args, and target, and its Dockerfile follows the Dockerfile rules above, except that the result need not be Debian- or Ubuntu-based. Other build keys, include, an extends that reads another file, models, and a platform that does not match the image’s architecture are rejected. So is an image pinned by digest. Use a tag instead.
  • An image a service pulls is pinned the way a FROM tag is (see Building and caching).
  • The Rust SDK doesn’t have compose_up. Use exec to run the command that compose_up_command returns.

Build an image 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.
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 _.
sail.Image.debian_amd64 and debian_arm64 also install a Python matching your local interpreter, so that @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.

Run services with systemd

Call with_systemd on an image and every Sailbox created from it runs systemd as its init system. A service you enable in the image starts with the Sailbox, and a unit that sets Restart=always is restarted whenever it exits.
The unit file is an ordinary systemd unit:
worker.service
  • Call it anywhere in the chain. The build installs systemd before your build steps run, so a step can run systemctl enable.
  • Services start when the Sailbox is created. A service may still be starting when create returns.
  • Any image can use it except a Compose project. It works on Sail’s Debian base, a registry image, and a Dockerfile image. The devbox image always runs systemd.
See with_systemd for how timers and reboot behave in a Sailbox.

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.
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, so they start quickly at every size.This makes two things part of the image contract:
  • Early boot runs during every hidden boot. In an image that runs systemd, units that are part of early boot, such as those wanted by sysinit.target, run there. They must be safe to run repeatedly, outside any Sailbox. The services you enable do not run there; they start in each Sailbox when it is created. 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.