# Upgrading

> Pull the new image and restart; migrations run on start. What survives, what only down -v removes, and how to pin a release.

Source: https://docs.getgraft.ai/graft/self-hosting/upgrading

Upgrading self-hosted Graft is two commands from the directory that holds your compose file:

```bash
docker compose pull && docker compose up -d
```

`pull` fetches the server image and the sandbox image at the tag `GRAFT_IMAGE_TAG` names, `latest` unless you set one. `up -d` recreates the `graft` container on the new image; Postgres is untouched. On start, before it listens, the server checks the committed migration chain for holes and applies every migration the database has not seen. The log line beginning `migrations: the chain of` says how many are applied and through which tag. A second start over the same database applies nothing and says so.

## What survives

Everything you care about lives in two named volumes, and `docker compose down` removes neither.

- **`<project>_toolboxes`** holds every person's toolbox: every published version of every tool with its installed packages and lockfile, and the drafts. The server mounts it whole and each sandbox mounts its own subpath of it. Only `docker compose down -v`, `docker volume rm` or a `docker volume prune` would remove it.
- **`<project>_graft_postgres_data`** holds the database: accounts, agents, connections with their encrypted credentials, working sets, approvals, pending actions and the acquire traces.

The `<project>` prefix is `graft` unless you ran the compose file under another project name.

> **Warning**
>
> Keep `GRAFT_KEYRING_SECRET` as it was. The local keyring derives its encryption key from it, so credentials encrypted under one value cannot be read under another. Back it up beside the volumes.

Changing `GRAFT_AUTH_SECRET` signs everyone out, since it signs every session. Changing `GRAFT_HANDOFF_SECRET` only invalidates handoff links not yet opened. `GRAFT_ADMIN_EMAIL` and `GRAFT_ADMIN_PASSWORD` act only on an empty database, so editing them after the first start changes nothing and the boot log says `admin bootstrap skipped`.

## Pinning a release

A release tags both images the same way: `1.2.3`, `1.2` and `1`, plus `sha-<commit>` for tracing an image to a commit. `latest` follows the newest release that is not a prerelease; a prerelease such as `1.2.3-rc.1` gets its own tag and does not move `latest`. To stay on a release, set it in `.env`:

```dotenv
GRAFT_IMAGE_TAG=1.2.3
```

Then `docker compose pull && docker compose up -d` moves to exactly that release and no further. `ghcr.io/getmodern-ai/graft` and `ghcr.io/getmodern-ai/graft-sandbox` follow the same tag, so the server and the image its sandboxes run from always match.

## Sandboxes across a restart

The sandboxes the server creates are containers of their own, `graft-sandbox-agent-<id>`, attached to the internal sandbox network. `docker compose down` does not remove them, and it refuses to remove the network while they exist, saying so. Either is fine:

```bash
docker rm -f $(docker ps -aq --filter label=graft.sandbox.prefix)   # remove them first
```

or leave them, and the next `docker compose up -d` finds them again. A sandbox is recreated from the image when needed and mounts its toolbox from the volume, so removing one loses no published tool.

## Checking the result

```bash
docker compose logs graft
```

The boot line `graft server listening on http://localhost:3000` reports the backings in use, whether the sandbox and the key pair are configured, the sweep interval and which model answers `acquire`. `GET /api/health` answers `{"ok":true}` once the server is up, and the compose health check polls it.

If a new image refuses to start, the log names the variable. A release that adds a required setting says so in exactly that line, and the [Configuration](https://docs.getgraft.ai/graft/self-hosting/configuration.md) page carries the current list.
