Self-hosted Graft is one Docker image and a compose file. docker compose up brings up Postgres and Graft, which runs the server, the proxy, the MCP endpoint and the console in one container, and builds the sandbox image that authored tools run in. Everything below is the repository’s own walkthrough, so if the two ever disagree the README wins. You need three things: Docker 26 or later with Compose, a model provider key from Anthropic or OpenAI, and an agent: Hermes or OpenClaw beside it, or Claude and ChatGPT once the deployment is behind a public https domain, since a chat product cannot reach localhost. The self-hosted form always brings its own model key and refuses to start without one.
1

Get the compose file and mint the secrets

The repository holds docker-compose.yml, its .env.example and the sandbox image’s Dockerfile; nothing else is needed from it.
The last line runs the image once, without Postgres, and appends the five secrets Graft refuses to start without: GRAFT_AUTH_SECRET, GRAFT_KEYRING_SECRET, GRAFT_HANDOFF_SECRET, GRAFT_CAPABILITY_TOKEN_PRIVATE_KEY and GRAFT_CAPABILITY_TOKEN_PUBLIC_KEY.
2

Set the model and the admin

Open .env and fill the model group:
GRAFT_MODEL_PROVIDER is anthropic or openai. The two model ids are optional and default per provider; GRAFT_MODEL_BASE_URL points the openai provider at an OpenAI-compatible gateway. Then change GRAFT_ADMIN_EMAIL and GRAFT_ADMIN_PASSWORD from the defaults. They open the one account a fresh database starts with, and they act only on an empty database.
Change the admin email and password before the console is reachable from anywhere but this machine. The values in .env.example are development defaults.
3

Bring it up

Postgres comes up first, then Graft. Read the log for two lines: one beginning migrations: the chain of, saying the committed migrations are applied, then admin <your email> created from GRAFT_ADMIN_EMAIL. After them comes graft server listening on http://localhost:3000.If the environment is incomplete, Graft refuses to start and names each missing variable on its own line:
The same refusal covers a missing model key. The image runs as production, and a production self-host without GRAFT_MODEL_BACKEND=provider, GRAFT_MODEL_PROVIDER and GRAFT_MODEL_API_KEY does not boot.
4

Sign in and create an agent

Open http://localhost:3000, sign in as the admin, then Agents → New agent. Give it a name, keep the cap and idle window at their defaults, and tick the connections it may use, if you have any yet. The token is shown once; put it where your harness reads environment variables. For Hermes:
If the token is lost, revoke the agent and create another. Graft keeps only its hash.
5

Point your harness at it

In ~/.hermes/config.yaml, under mcp_servers, then /reload-mcp in a session or restart Hermes:
Your harness now lists Graft’s meta-tools, acquire, find_tool, promote, demote, run_tool and the rest, beside its own.
6

Ask for something no tool covers

The agent proposes a connection with request_connection and hands you a link into the console to confirm it. Then acquire reads the vendor’s documentation, writes the module, checks it, dry-runs it against the live API with writes stopped at the proxy, publishes it and promotes it. The new tool appears in your harness’s list as httpbin__<name>, first-class. When a vendor needs a secret, you enter it in the console and never in the chat.

What is running

docker compose ps shows two services. postgres is postgres:18 with its data in the graft_postgres_data volume. graft is the published image ghcr.io/getmodern-ai/graft, serving the console at /, the JSON API under /api, the proxy at /api/proxy and the MCP endpoint at /mcp, with its health check on GET /api/health. A third entry in the compose file, sandbox, is not a running service: it builds or pulls the sandbox image so the server can create sandboxes from it. Sandboxes are containers of their own, named graft-sandbox-agent-<id>, on an internal network named <project>_sandbox. That network has no gateway and no external DNS; its only other member is the graft container under the alias proxy, which is the one route out. Every person’s toolbox lives in the <project>_toolboxes volume, mounted into the server and into each sandbox by its own subpath, so the files the server writes and the tree a sandbox runs are one.
The graft container mounts the host’s Docker socket to create sandboxes. That is root-equivalent access to the host, and the reason to run Graft as a dedicated user on a dedicated host. The alternative, a sibling Docker daemon, is described below.

Variations

Behind a domain. Set GRAFT_PUBLIC_URL in .env to the https origin your reverse proxy serves, and put that proxy in front of port 3000. Sign-in cookies and every handoff URL are built on it. On Linux. The Docker socket is owned by root:docker, so the unprivileged server needs the socket’s group. Set GRAFT_DOCKER_GID to the output of stat -c %g /var/run/docker.sock. Under Docker Desktop the default 0 is right. Without the socket. A Docker daemon in a sibling container, docker:dind, works too: the server takes DOCKER_HOST instead of the socket mount, and the sandbox network, the volumes and the sandbox image all live inside that daemon. packages/sandbox-docker/README.md in the repository describes the arrangement and what has to live inside the sibling daemon. Ports. GRAFT_PORT moves the console, API, proxy and MCP endpoint off 3000. GRAFT_POSTGRES_PORT moves the published Postgres port, which exists only for a server run from source on the host; the graft container reaches Postgres by name and never through it. A second copy. The compose file sets no container names, so docker compose -p <name> up runs a second Graft beside the first with its own containers, network and volumes. Stopping. docker compose down stops Postgres and Graft. The sandboxes the server created are separate containers still attached to the sandbox network, and down says so rather than removing them. Remove them first with docker rm -f $(docker ps -aq --filter label=graft.sandbox.prefix), or leave them for the next up, which finds them again. Neither touches the toolbox volume; see Upgrading.

Where next

Configuration

Every variable the server reads, with its default and when it is required.

Upgrading

Pull, restart, and what survives a restart.

Hermes

The skill, the config block and how approvals appear.

Connecting a vendor

How an agent proposes a connection and you enter the secret.