# Run Graft with Docker Compose

> Ten minutes from a machine with Docker to an agent that acquires its own tools, using the compose file in the Graft repository.

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

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](https://github.com/getmodern-ai/graft#self-hosting) 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.

### Step 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.

```bash
git clone https://github.com/getmodern-ai/graft && cd graft
cp .env.example .env
docker compose run --rm --no-deps graft node dist/keys.mjs >> .env   # five secrets, one line each
```

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`.

### Step 2: Set the model and the admin

Open `.env` and fill the model group:

```dotenv
GRAFT_MODEL_BACKEND=provider
GRAFT_MODEL_PROVIDER=openai
GRAFT_MODEL_API_KEY=sk-...
GRAFT_MODEL_AUTHORING=gpt-5.6-sol
GRAFT_MODEL_TRIAGE=gpt-5.4-mini
```

`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.

> **Warning**
>
> Change the admin email and password before the console is reachable from anywhere but this machine. The values in `.env.example` are development defaults.

### Step 3: Bring it up

```bash
docker compose up -d
docker compose logs graft
```

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:

```text
graft refused to start: the environment is invalid.

GRAFT_AUTH_SECRET is not set
```

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.

### Step 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:

```bash
echo "GRAFT_TOKEN=grft_…" >> ~/.hermes/.env
```

If the token is lost, revoke the agent and create another. Graft keeps only its hash.

### Step 5: Point your harness at it

**Hermes**

In `~/.hermes/config.yaml`, under `mcp_servers`, then `/reload-mcp` in a session or restart Hermes:

```yaml
mcp_servers:
  graft:
    url: "http://localhost:3000/mcp"
    headers:
      Authorization: "Bearer ${GRAFT_TOKEN}"
```

**OpenClaw**

OpenClaw's Gateway reads `mcp.servers` from `~/.openclaw/openclaw.json`, the transport said outright, and the token from `~/.openclaw/.env`; the console shows this block under **Connect a harness** with **OpenClaw** chosen. Restart the Gateway so it loads the token.

```json
{
  "mcp": {
    "servers": {
      "graft": {
        "url": "http://localhost:3000/mcp",
        "transport": "streamable-http",
        "headers": { "Authorization": "Bearer ${GRAFT_TOKEN}" }
      }
    }
  }
}
```

OpenClaw has no MCP elicitation, so approvals arrive as console links rather than buttons.

Your harness now lists Graft's meta-tools, `acquire`, `find_tool`, `promote`, `demote`, `run_tool` and the rest, beside its own.

### Step 6: Ask for something no tool covers

```text
Get me a fresh UUID from httpbin.org.
```

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.

> **Note**
>
> 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](https://docs.getgraft.ai/graft/self-hosting/upgrading.md).

## Where next

- [Configuration](https://docs.getgraft.ai/graft/self-hosting/configuration.md): Every variable the server reads, with its default and when it is required.
- [Upgrading](https://docs.getgraft.ai/graft/self-hosting/upgrading.md): Pull, restart, and what survives a restart.
- [Hermes](https://docs.getgraft.ai/graft/harnesses/hermes.md): The skill, the config block and how approvals appear.
- [Connecting a vendor](https://docs.getgraft.ai/graft/connections/connecting-a-vendor.md): How an agent proposes a connection and you enter the secret.
