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

# Snapshots & Copies

> How a sandbox persists, and the four ways to bring one back: resume, fork, template, and download.

A snapshot is a point-in-time copy of a sandbox's **filesystem**. You never take one by hand: Boat captures them continuously in the background. Everything else on this page is a different way of using them.

## Which one do I want?

Three things to do with a snapshot, and they get mixed up because they all come from the same one.

* **Resume** gives you your sandbox back. Same Sandbox, same id, new machine underneath.
* **Fork** gives you a second sandbox holding a copy of the filesystem as it is right now. The original keeps running, untouched.
* **Template** freezes a named copy that stays put. You deploy from it whenever you like, and it still works long after the sandbox it came from is gone.

```mermaid theme={null}
flowchart LR
  A[Sandbox filesystem] -->|every minute, and on stop| B[(Snapshots)]
  B -->|boat resume| C[Same Sandbox, new machine]
  B -->|boat fork| D[New Sandbox, copy of the filesystem as it is now]
  B -->|boat snapshot id name| E[Named template, frozen and kept]
  E -->|boat new --from name| F[New Sandbox, copy of that frozen point]
  B -->|boat snapshot pull| G[Files on your laptop]
```

Forking and deploying both give you a new Sandbox with a copy of a filesystem, and you can do either as many times as you want. What differs is **which** copy: a fork takes the sandbox as it is at that moment and needs the sandbox to still exist, while a template is a point you named and froze, which you can return to weeks later and long after the original is gone.

A rule of thumb: **resume** when you want your sandbox back, **fork** when you want a second one from where it stands right now, **template** when you want a fixed starting point to come back to.

## What is captured

<Columns cols={2}>
  <Card title="Captured" icon="check">
    `/home/user`: your code, files, and config

    Docker **named volumes** (`/var/lib/docker/volumes`)

    **Your changes** under `/etc`, `/usr`, `/opt`, `/root`, `/srv`, cron tables, and the apt package database: installed packages, systemd services, system config
  </Card>

  <Card title="Not captured" icon="xmark">
    The base OS and pre-installed tooling, which ship with the machine image

    Machine identity: hostname, network config, SSH host keys

    Running processes, memory, open ports

    Docker **build cache**, and images no container uses (`/var/lib/docker`; named volumes and the containers you had running, with their images, do come back)
  </Card>
</Columns>

Deletions count as changes. Remove a pre-installed coding agent (`sudo rm -rf /opt/kimi-code /usr/local/bin/kimi`, `sudo npm uninstall -g @openai/codex`) or an apt package, and it stays gone on every resume, fork and template deploy. Your own installs always shadow the pre-installed copies.

Coming back from a snapshot behaves like rebooting a server. Your files, installed packages and system setup return, and **systemd services you enabled start again on their own**. Processes you ran by hand do not survive; restart them, or make them a service. Snap packages are recorded and reinstalled. See [Long-Running Tasks](/long-running-tasks).

### Docker builds across resume and fork

Docker and BuildKit are installed, and `docker build` uses the layer cache normally while a sandbox runs: a rebuild with nothing changed takes well under a second. That cache lives in `/var/lib/docker`, which is not captured, so the first build after a resume or on a fork starts from scratch and pulls its base images again. Containers you had running are recreated with the image they were using; only the build cache is lost.

To keep the cache, export it into your home directory, which is captured:

```bash theme={null}
docker buildx build \
  --cache-to type=local,dest=$HOME/.buildcache,mode=max \
  --cache-from type=local,src=$HOME/.buildcache \
  --load -t myapp .
```

Measured on a Node app whose dependency install is the slow step: 38 s cold, 0.4 s rebuilt on the same machine, 8 s rebuilt on a fork with the exported cache. Named volumes keep their data either way.

### Excluding files with `.boxignore`

Build artifacts and dependency trees are worth nothing in a snapshot and slow every restore down. Nothing is left out of a snapshot unless you ask for it: write a `.boxignore` and every capture from the next one on obeys it.

```bash theme={null}
cd ~/myapp
printf 'node_modules/\n.next/\n' > .boxignore
```

It uses gitignore syntax, and its rules are relative to the directory it sits in, so a `.boxignore` in a repo covers that repo and one in your home directory covers the whole sandbox. It is looked for up to six directories below your home directory, and the search skips `node_modules`, `.next`, `target` and `vendor`. Put the file at the root of the tree you want skipped, not inside it.

**Your `.gitignore` is not used.** It answers "do not commit this", which is a different question from "it is safe to lose this on resume", and a build cache is routinely both. It is also not yours at all in a tool you installed as a git clone: `~/.nvm`'s own `.gitignore` excludes `v*`, which is every node version you have installed.

`.git` is always captured, since it is the one thing you cannot regenerate.

Sandboxes created before this file was renamed still honour the old `.oneignore` name.

## Automatic snapshots

Snapshots are **incremental**: the first is a full base, each later one stores only what changed, compressed, content-addressed and deduped against the base image. They are taken **every minute** while the sandbox is ready or idle, and a final one when it **stops**.

A snapshot does not reserve a second copy of your entire root disk. Capture uploads
compressed chunks; restore extracts chunks onto a fresh machine and removes staging
files as they are consumed. The [machine restore budgets](/machines) apply to the
uncompressed data restored onto that machine, not the compressed download size.

If that final snapshot fails, the stop is aborted and the machine keeps running, so stopping can never lose data. Stopping at any moment is safe; the snapshot is always complete.

Snapshots are kept for the **life of the sandbox**. Its latest can be resumed or forked whether it stopped yesterday or months ago. Superseded ones are cleaned up continuously as new ones are taken.

### When a stop is refused

Stopping saves the disk first, and if that save is failing we refuse the stop and leave the sandbox running rather than throw away your work. We retry on our own and email you.

**Billing pauses by itself.** The meter stops at the first refused attempt, in the balance you see as well as on your invoice. A Sandbox held open by a broken snapshot costs you nothing from that moment on, so there is nothing to claim and no refund to ask for. The pause lifts when you start using the sandbox again.

If it keeps happening, snapshots are genuinely failing on that sandbox. We are alerted automatically, but contact us so we look at yours. To stop anyway and accept losing everything written since the last successful snapshot, pass `--force`.

## Resume

Brings the same sandbox back on a fresh machine, from its latest snapshot. Same Sandbox id, same filesystem, new hardware.

<CodeGroup>
  ```bash CLI theme={null}
  boat resume bx_f7k2q9hd
  boat resume bx_f7k2q9hd --type large      # different machine size
  boat resume bx_f7k2q9hd --ttl 7200        # new lifetime, in seconds
  boat resume bx_f7k2q9hd --no-auto-stop    # runs until you stop it
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd/resume" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"type":"large","ttlSeconds":7200}'
  ```

  ```ts TypeScript theme={null}
  await sandbox.resume({
    sandboxId: "bx_f7k2q9hd",
    resumeRequest: { type: "large", ttlSeconds: 7200 },
  });
  ```

  ```python Python theme={null}
  sandbox.resume(sandbox_id="bx_f7k2q9hd", resume_request=ResumeRequest(type="large", ttl_seconds=7200))
  ```
</CodeGroup>

Requires a completed snapshot, which means the sandbox was stopped cleanly with `boat stop`. Shrinking to a smaller machine is refused if the sandbox holds more data than it can take, and the sandbox is left untouched. See [Machines](/machines).

Omit `ttlSeconds` to keep the sandbox's current lifetime. Pass `null` (`--no-auto-stop`) to switch auto-stop off entirely.

## Fork

Clones a sandbox from its latest snapshot into a new, independent sandbox. The source keeps running and is never modified.

<CodeGroup>
  ```bash CLI theme={null}
  boat fork bx_f7k2q9hd
  boat fork bx_f7k2q9hd --type small
  boat fork bx_f7k2q9hd --ttl 600         # lifetime for the fork, in seconds
  boat fork bx_f7k2q9hd --no-auto-stop    # runs until you stop it
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd/fork" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"type":"small","ttlSeconds":600}'
  ```

  ```ts TypeScript theme={null}
  const forked = await sandbox.fork({
    sandboxId: "bx_f7k2q9hd",
    forkRequest: { type: "small", ttlSeconds: 600 },
  });
  ```

  ```python Python theme={null}
  forked = sandbox.fork(sandbox_id="bx_f7k2q9hd", fork_request=ForkRequest(type="small", ttl_seconds=600))
  ```
</CodeGroup>

The fork inherits the whole filesystem and the source's per-Sandbox variables unless you pass your own `env`. It also inherits the source's exact environment version, so a fork never picks up configuration its source never had. See [Environments](/environments).

A fork does **not** inherit the source's lifetime. It defaults to 1 hour, so a fork of a sandbox with auto-stop switched off is not itself left running forever. Pass `ttlSeconds` (or `--no-auto-stop`) when you want something else.

Use fork for a throwaway copy right now: a second branch of work, a risky experiment, one machine per user of your product. If you find yourself forking the same sandbox repeatedly, make it a template instead.

## Template Sandboxes

When many sandboxes need the same stack pre-installed, build it once, save it under a name, and deploy from that name instead of installing on every fresh sandbox.

1. Create a sandbox and install everything: runtimes, packages, your app or daemon.
2. Save it: `boat snapshot <id> <name>`. That freezes the sandbox's disk at this moment under the name.
3. For each new Sandbox: `boat new --from <name>`. Deploys are usable in a few seconds, at roughly constant cost regardless of how much the template holds.

<CodeGroup>
  ```bash CLI theme={null}
  boat new                            # install your stack, then:
  boat snapshot current web-stack     # freeze it under a name
  boat new --from web-stack           # deploy as many as you want
  boat snapshots                      # your named snapshots, then capture history
  boat snapshot rm web-stack          # remove it and release its storage
  ```

  ```bash curl theme={null}
  curl -sS -X POST "$BOAT_API_BASE/named-snapshots" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"sandboxId":"bx_23456789","name":"web-stack"}'

  curl -sS -X POST "$BOAT_API_BASE/sandboxes" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"from":"web-stack","environment":"users","env":{"TENANT_ID":"acme"}}'
  ```

  ```ts TypeScript theme={null}
  await sandbox.saveNamedSnapshot({
    namedSnapshotSaveRequest: { sandboxId: "bx_23456789", name: "web-stack" },
  });

  // poll until it settles at "ready"
  const saved = await sandbox.getNamedSnapshot({ name: "web-stack" });

  await sandbox.create({
    createSandboxRequest: { from: "web-stack", environment: "users", env: { TENANT_ID: "acme" } },
  });

  await sandbox.listNamedSnapshots();
  await sandbox.deleteNamedSnapshot({ name: "web-stack" });
  ```

  ```python Python theme={null}
  from boat_sdk.models.named_snapshot_save_request import NamedSnapshotSaveRequest

  sandbox.save_named_snapshot(NamedSnapshotSaveRequest(sandbox_id="bx_23456789", name="web-stack"))

  # poll until it settles at "ready"
  saved = sandbox.get_named_snapshot("web-stack")

  sandbox.create(CreateSandboxRequest.from_dict({
      "from": "web-stack",
      "environment": "users",
      "env": {"TENANT_ID": "acme"},
  }))

  sandbox.list_named_snapshots()
  sandbox.delete_named_snapshot("web-stack")
  ```
</CodeGroup>

<Note>
  `from` is a reserved word in Python, so build that one request with `from_dict` as above rather than keyword arguments.
</Note>

Saving from a running sandbox captures its current disk, taking a moment while a fresh snapshot lands. Saving from a stopped sandbox freezes its last snapshot.

A named snapshot is a frozen copy, independent of the sandbox it came from. The source can keep changing, stop, or disappear, and the name still deploys the exact state you saved. Independent means independent: a sandbox's snapshots form a chain that is periodically rebuilt from a fresh base with the old links cleaned up, and none of that touches your saved name. The save takes its own complete copy of everything needed to restore, so it never has to walk the source sandbox's chain, and underlying data both still need is kept for as long as either one refers to it. A saved name has no expiry.

You can keep up to 10, and each shows its size in `boat snapshots`.

### Templates are not environments

A template holds the **disk**. An [environment](/environments) holds the **configuration**. They are different tools and most setups use both.

|             | Template Sandbox                                    | Environment                                             |
| ----------- | --------------------------------------------------- | ------------------------------------------------------- |
| Holds       | Installed packages, builds, caches, warm state      | Repositories to clone, secrets, credentials             |
| Answers     | "What is already on this machine?"                  | "What is this machine allowed to have?"                 |
| Costs       | Storage, and a few seconds per deploy               | Nothing                                                 |
| Changing it | Re-save the name. Deployed Sandboxes are unaffected | Mints a version. Running Sandboxes move only on upgrade |
| Applies at  | Deploy time, once                                   | Every start, resume, and fork                           |

If you would put it in a Dockerfile it belongs in a template; if you would put it in a `.env` it belongs in an environment. Compose them:

```bash theme={null}
boat new --from web-stack --environment users --env TENANT_ID=acme
```

That Sandbox boots with your stack already installed, none of your credentials inside it, and one variable of its own.

<Warning>
  Deploying from a template does not carry the source sandbox's **named environment**. The new Sandbox is pinned by `--environment`, or by your default environment when you omit it. A template built while you were logged into your own GitHub does not leak that access to its deploys.

  Its **per-Sandbox variables do carry**, the same way they do on a fork: anything you passed as `--env KEY=VALUE` to the source sandbox is set on every deploy from that name, unless the deploy passes its own `env`. Do not put a secret in `--env` on a sandbox you are about to save as a template.
</Warning>

### Updating a template

Save the same name again: resume the sandbox (or any sandbox set up the way you want), update the stack, and run `boat snapshot <id> <name>` with the existing name. The name points at the new state and the old artifact is released. Sandboxes already deployed from it are unaffected. If a re-save fails, the name keeps deploying the last good save.

## What happens on restore

A resume, fork or deploy is usable in a **few seconds, whatever the sandbox holds**. The full file tree is there immediately, every file is readable on demand, and content finishes downloading in the background.

Permissions, ownership, timestamps and extended attributes come back with your files, and on directories too. Two details are worth knowing:

* **Extended attributes on a file** become readable once that file's content has arrived. Directories carry theirs from the moment the sandbox is up.
* **Directory modification times** are restored once the background download finishes rather than immediately, because writing a file into a directory updates that directory's timestamp, so setting it any earlier would just be overwritten.

### Warming for faster first boots

A Sandbox records the order in which files are first opened while it is starting up, and keeps that order in `.ascii/playbook.json`. On the next start those files are fetched first, so your app reaches a working state before the rest of the disk has arrived. The playbook is an ordinary file, so it is captured into the snapshot and every fork or deploy inherits it.

Recording only happens **while a sandbox is starting up from a snapshot**, so the order matters:

<Steps>
  <Step title="Start the sandbox from a snapshot">
    Resume a stopped sandbox, or deploy one from the template you are about to update. A Sandbox created from scratch has nothing to record against.

    ```bash theme={null}
    boat resume bx_f7k2q9hd
    ```
  </Step>

  <Step title="Boot your app straight away">
    Run the normal startup, right away, while the sandbox is still filling in. Every file it opens is recorded in the order it asks for them. Files opened later, once the sandbox has finished filling in, are not recorded.

    ```bash theme={null}
    boat exec bx_f7k2q9hd "cd my-repo && npm run dev"
    ```
  </Step>

  <Step title="Let it finish, then save">
    The playbook is written when the sandbox finishes filling in. Save after that, or the run you just did is not in the template.

    ```bash theme={null}
    boat snapshot bx_f7k2q9hd web-stack
    ```
  </Step>
</Steps>

Each run merges into the previous playbook rather than replacing it, with the newest run weighted highest, so a template warms up further every time you repeat this. It holds the first 5000 paths.

## Retention

By default, snapshots are kept for the **life of the archived sandbox**: its latest snapshot can be resumed or forked whether it stopped yesterday or months ago. Superseded snapshots are cleaned up continuously as new ones are taken.

Permanent deletion is different from archive. Deleting a sandbox or snapshot returns a background operation and makes the target unavailable; it cannot be resumed. Named snapshots remain independent of their source sandbox. When you remove one, its backing data is scheduled no earlier than six hours later so already-issued signed upload URLs expire first.

With [zero data retention](/data-retention), archived sandbox data and named snapshots are queued for deletion instead of retained.

## Inspect and download

<CodeGroup>
  ```bash CLI theme={null}
  boat snapshots                                 # across your sandboxes
  boat snapshot latest bx_f7k2q9hd
  boat snapshot tree <snapshotId>                # files + sizes
  boat snapshot pull <snapshotId> -o ./restore   # download and reassemble
  ```

  ```bash curl theme={null}
  curl -sS "$BOAT_API_BASE/snapshots" \
    -H "Authorization: Bearer $BOAT_API_KEY"
  curl -sS "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd/snapshots/latest" \
    -H "Authorization: Bearer $BOAT_API_KEY"
  curl -sS "$BOAT_API_BASE/snapshots/<snapshotId>/tree" \
    -H "Authorization: Bearer $BOAT_API_KEY"

  # grab one file (or a folder as .tar) straight from a snapshot; sandbox can be stopped
  curl -sS "$BOAT_API_BASE/snapshots/<snapshotId>/files?path=projects/app/.env" \
    -H "Authorization: Bearer $BOAT_API_KEY" -o .env
  curl -sS "$BOAT_API_BASE/snapshots/<snapshotId>/files?path=projects/app" \
    -H "Authorization: Bearer $BOAT_API_KEY" -o app.tar
  ```

  ```ts TypeScript theme={null}
  await sandbox.listSnapshots();                                   // across your sandboxes
  await sandbox.listSandboxSnapshots({ sandboxId: "bx_f7k2q9hd" });        // one sandbox's history
  await sandbox.getLatestSandboxSnapshot({ sandboxId: "bx_f7k2q9hd" });
  await sandbox.getSnapshotTree({ snapshotId: "<snapshotId>" });   // files + sizes

  const file = await sandbox.getSnapshotFile({
    snapshotId: "<snapshotId>",
    path: "projects/app/.env",
  });
  const archive = await sandbox.getSnapshotDownload({ snapshotId: "<snapshotId>" });
  ```

  ```python Python theme={null}
  sandbox.list_snapshots()                          # across your sandboxes
  sandbox.list_sandbox_snapshots("bx_f7k2q9hd")         # one sandbox's history
  sandbox.get_latest_sandbox_snapshot("bx_f7k2q9hd")
  sandbox.get_snapshot_tree("<snapshotId>")         # files + sizes

  file = sandbox.get_snapshot_file("<snapshotId>", path="projects/app/.env")
  archive = sandbox.get_snapshot_download("<snapshotId>")
  ```
</CodeGroup>

`pull` writes two subfolders, `home_user/` (your `/home/user`) and `docker/` (named volumes), reflecting exactly the files that were live at that snapshot. There is no SDK equivalent of `pull`: it is `getSnapshotDownload` plus local reassembly.

Browse the same tree, and download files from it, on the [Snapshots](https://boat.dev/dashboard?tab=snapshots) tab of the dashboard.

See the [Snapshots API](/api/reference/snapshots/list-snapshots) for the full surface.

## Deleting a sandbox's snapshots

Stopping a sandbox keeps its snapshots, that is the whole point of stopping. **Deleting** a sandbox throws them away.

<Warning>
  Deleting a sandbox force-stops it and permanently deletes its snapshots. It cannot be resumed, forked or recovered afterwards, and there is no deleted-Sandboxes list to restore from.

  **If you want the data usable later, stop the sandbox instead of deleting it.** A stopped sandbox is free, keeps its disk, and resumes where you left off.
</Warning>

<CodeGroup>
  ```bash CLI theme={null}
  boat stop bx_f7k2q9hd            # keeps the data; `boat resume` brings it back
  boat delete bx_f7k2q9hd          # deletes the sandbox AND its snapshots, permanently
  boat delete bx_f7k2q9hd --yes    # same, without the confirmation prompt
  ```

  ```bash curl theme={null}
  curl -sS -X DELETE "$BOAT_API_BASE/sandboxes/bx_f7k2q9hd" \
    -H "Authorization: Bearer $BOAT_API_KEY" \
    -H "X-Ascii-Confirm-Delete: bx_f7k2q9hd"
  ```

  ```ts TypeScript theme={null}
  await sandbox.deleteSandbox({
    sandboxId: "bx_f7k2q9hd",
    xAsciiConfirmDelete: "bx_f7k2q9hd",
  });
  ```

  ```python Python theme={null}
  sandbox.delete_sandbox(
      x_ascii_confirm_delete="bx_f7k2q9hd",
      sandbox_id="bx_f7k2q9hd",
  )
  ```
</CodeGroup>

Every delete has to name its own target: `X-Ascii-Confirm-Delete` must equal the sandbox id exactly, or the request is refused with `409` and nothing is deleted. See [Data retention and deletion](/data-retention) for the operation you get back and how to poll it.

You can also delete a sandbox from the `⋯` menu on its row in the [dashboard](https://boat.dev/dashboard?tab=sandboxes).

The Sandbox leaves your account immediately; the snapshot data goes once the machine has finished shutting down.

**What survives, and why.** Snapshots are shared: a fork, a resume and a deploy from a template all read the *same* physical objects as the sandbox they came from. Deleting a sandbox therefore only removes the snapshot data nothing else is using. Kept are:

* chains a sandbox **forked or resumed from this one** still restores from
* **named snapshots** saved from this sandbox, which are meant to outlive it. Delete those with `boat snapshot rm <name>` when you want the bytes gone.

Storage is released once the last sandbox or named snapshot using a chain is gone.

## Why filesystem, not the VM

Snapshots capture the filesystem, independent of the machine running underneath. Today a sandbox is a Hetzner VPS, but a sandbox is meant to become anything: a Linux server, a Mac or Windows machine in the cloud, or a physical device. A filesystem snapshot stays portable across all of them, restores fast, stays small, and lets us filter what is captured as needs grow. A whole-VM image would tie you to one kind of machine.

## Related

* [Environments](/environments)
* [Build a Platform on Boat](/platform-guide)
* [Long-Running Tasks](/long-running-tasks)
* [Machines](/machines)
* [Data retention and deletion](/data-retention)
