Upgrading self-hosted Graft is two commands from the directory that holds your compose file:
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.
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:
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:
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

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 page carries the current list.