# Self-hosting the engine

> Page: https://docs.plotwork.dev/mcp/self-hosting. Index of every page: https://docs.plotwork.dev/llms.txt

Run the Plotwork engine inside your own network as one container: the MCP tools for your agents and the REST drawing routes for your services, with no account and no request leaving your network. It is for platform and security teams at companies that cannot send diagram text to a hosted service.

## TL;DR

| Question | Answer |
|---|---|
| What is it? | One container image with the diagram engine, MCP over Streamable HTTP at `/mcp`, the REST drawing routes under `/v1`, and `/healthz`. |
| Does anything leave my network? | No. The server makes no outbound request, sends no telemetry, and stores nothing. Logs go to standard output and never hold diagram text. |
| What is not in it? | Accounts, the portal, hosted diagram links, organisations, brand themes, and icon packs. Those are features of the hosted service. |
| How do I run it? | Put the image in your registry, set `PLOTWORK_TOKEN`, and start it with Docker, Compose, or Kubernetes. |
| How does an agent connect? | Point the client at `http://<host>:3106/mcp` with the header `Authorization: Bearer <token>`. |
| Is it the same engine? | Yes. The tools, their schemas, and the pictures are the same as the hosted service and the npm package. |

## What is included

| Part | Self-hosted | Hosted service |
|---|---|---|
| Every MCP tool of the local server (draw, edit, convert, import, map icons, sync Markdown, patterns, read, compare, validate, render, outline, search icons) | Yes | Yes |
| REST: validate, render, draw, edit, convert Mermaid, sync Markdown, find, get, and draw a pattern | Yes, same paths and bodies | Yes |
| `read_diagram` and `compare_diagrams` with a link to a hosted picture | No: send the SVG text or the source | Yes |
| A `viewUrl` on a drawing | No: it points at Plotwork's viewer | Yes |
| Publishing, listing, and deleting hosted diagrams | No | Yes |
| Accounts, API keys, the portal, organisations | No | Yes |
| Brand themes and icon packs | No | Yes, on plans that include them |
| Usage counting of any kind | No | Anonymous daily totals |

The REST routes take the same request bodies and answer the same shapes as the [hosted API](https://docs.plotwork.dev/api/reference.md), so a client written for one works with the other after a change of base address and credential. The self-hosted server adds two answers the hosted API does not have: `503 engine.busy` when every worker is busy and the queue is full, and `504 engine.timeout` when a job runs past its deadline.

## 1. Get the image

Plotwork gives your organisation the image. Push it into your own registry, so your hosts pull it from inside your network. The examples on this page call it `plotwork-selfhost`.

| Property | Value |
|---|---|
| Base | Distroless Node 24, pinned by digest |
| Size | About 56 MB compressed |
| User | 65532, not root |
| Filesystem | Runs with a read-only root filesystem; writes nothing |
| Port | 3106 |
| Health check | Built in: `GET /healthz` over loopback |

## 2. Run it

Make a token and keep it in your secret store:

```sh
openssl rand -hex 32
```

Run the container with the token in its environment:

```sh
docker run -d --name plotwork \
  --read-only --cap-drop ALL --security-opt no-new-privileges \
  -p 127.0.0.1:3106:3106 \
  -e PLOTWORK_TOKEN \
  plotwork-selfhost
```

Check it:

```sh
curl http://127.0.0.1:3106/healthz
```

### With Compose

This file runs the server on an internal network, which has no route out. Docker publishes no port from an internal network, so the services that call Plotwork, such as an agent gateway or a reverse proxy, join the `plotwork` network and reach it at `http://plotwork:3106`. Put `PLOTWORK_TOKEN` in a `.env` file beside it, never in the file itself.

```yaml
services:
  plotwork:
    image: plotwork-selfhost
    restart: unless-stopped
    environment:
      PLOTWORK_TOKEN: ${PLOTWORK_TOKEN:?Set PLOTWORK_TOKEN in .env}
      PLOTWORK_WORKERS: '2'
    networks:
      - plotwork
    read_only: true
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    mem_limit: 1g
    pids_limit: 64

networks:
  plotwork:
    internal: true
```

### Settings

Every setting is an environment variable. A value the server cannot use stops it at start with a message that names the variable.

| Variable | Default | Meaning |
|---|---|---|
| `PLOTWORK_TOKEN` | none | The bearer token for `/mcp` and `/v1`, at least 32 characters. |
| `PLOTWORK_HOST` | `0.0.0.0` in the image | The address to listen on. |
| `PLOTWORK_PORT` | `3106` | The port to listen on. |
| `PLOTWORK_ALLOW_OPEN` | unset | `1` serves without a token on an address other than loopback. Use it only where the network itself is the control. |
| `PLOTWORK_ALLOWED_ORIGINS` | none | Browser origins allowed to call, separated by commas. |
| `PLOTWORK_WORKERS` | 2 | Worker threads that run the engine. One layout uses one thread for up to a second. |
| `PLOTWORK_QUEUE` | `32` | Jobs that may wait for a worker before more are refused. |
| `PLOTWORK_TIMEOUT_MS` | `30000` | The deadline of one job, waiting included. |
| `PLOTWORK_WORKER_MEMORY_MB` | `512` | The heap limit of each worker. |
| `PLOTWORK_MAX_BODY_BYTES` | `4194304` | The largest request body. |
| `PLOTWORK_MCP_RESULT_LIMIT` | `36000` | Characters of an MCP drawing answer before its pictures are left out, so an AI client can read it. |
| `PLOTWORK_TOOLS` | `full` | `core` lists only the eight workflow tools on `/mcp`, with short descriptions, for an open model with a small context. See [Use Plotwork with open and self-hosted models](https://docs.plotwork.dev/mcp/open-models.md). Any other value stops the start. |
| `PLOTWORK_LICENSE` | none | Your licence. `/healthz` shows whether one is set and a short fingerprint of it, never the value. |

Without `PLOTWORK_TOKEN`, the server listens on loopback only. The image listens on every address, so it does not start without a token unless `PLOTWORK_ALLOW_OPEN=1` is set.

## 3. Connect an agent

Every client below sends the token as a bearer header. Replace `plotwork.internal` with the address of your server.

### Claude Code

```sh
claude mcp add --transport http plotwork http://plotwork.internal:3106/mcp \
  --header "Authorization: Bearer $PLOTWORK_TOKEN"
```

### Cursor

Add this to `~/.cursor/mcp.json`:

```json
{
  "mcpServers": {
    "plotwork": {
      "url": "http://plotwork.internal:3106/mcp",
      "headers": {
        "Authorization": "Bearer ${env:PLOTWORK_TOKEN}"
      }
    }
  }
}
```

### VS Code

Add this to `.vscode/mcp.json`. VS Code asks for the token once and keeps it out of the file:

```json
{
  "inputs": [
    {
      "type": "promptString",
      "id": "plotwork-token",
      "description": "Plotwork token",
      "password": true
    }
  ],
  "servers": {
    "plotwork": {
      "type": "http",
      "url": "http://plotwork.internal:3106/mcp",
      "headers": {
        "Authorization": "Bearer ${input:plotwork-token}"
      }
    }
  }
}
```

### A service over REST

```sh
curl -X POST http://plotwork.internal:3106/v1/diagrams/render \
  -H "Authorization: Bearer $PLOTWORK_TOKEN" \
  -H "content-type: application/json" \
  -d '{"source": "flow \"Checkout\"\nnode web service \"Web\"\nnode db database \"Orders\"\nweb -> db"}'
```

## 4. Security notes

| Topic | What the server does | What you do |
|---|---|---|
| Transport | Plain HTTP. | Put TLS in front: a reverse proxy, a service mesh, or an ingress. |
| Authentication | One static bearer token, compared in constant time. A missing or wrong token is a 401. | Keep the token in a secret store, give each environment its own, and rotate it by restarting with a new one. |
| Outbound network | Makes no request. `fetch` and the socket clients are switched off in the process, and a link sent to a tool is refused. | Block egress anyway, with an internal network, a network policy, or a firewall rule, so the container has no route out. |
| Browsers | Refuses a request whose `Origin` is not in `PLOTWORK_ALLOWED_ORIGINS`, which also stops a web page from reaching a server on loopback. | List only the origins of your own tools. |
| Limits | A body cap, a bounded queue, a deadline on every job, and a heap cap on each worker. A job past its deadline is stopped and its worker replaced. | Set container memory and CPU limits; about 250 MB at rest with two workers. |
| Logs | One JSON line per request and per tool call: path, status, tool, outcome, duration, and argument size. Never diagram text, a header value, or an error message. | Collect standard output with your log pipeline. |
| Storage | None. Every request is worked on in memory and forgotten. | Run with a read-only root filesystem. |
| Telemetry | None. `PLOTWORK_USAGE`, which turns on counting in the npm package, does nothing here. | Nothing. |

## See also

| Doc | Why |
|---|---|
| [MCP server](https://docs.plotwork.dev/mcp/overview.md) | The tools, and how the local and hosted servers differ |
| [Connecting an agent](https://docs.plotwork.dev/mcp/connect.md) | Client settings for the hosted server and the npm package |
| [API reference](https://docs.plotwork.dev/api/reference.md) | Request and response shapes of the REST routes |
| [Privacy policy](https://docs.plotwork.dev/privacy-policy.md) | What the hosted service collects, none of which applies to a self-hosted server |
