Self-hosting the engine
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.
- Docker
- Claude Code
- Cursor
- VS Code
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, 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:
openssl rand -hex 32Run the container with the token in its environment:
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-selfhostCheck it:
curl http://127.0.0.1:3106/healthzWith 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.
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: trueSettings
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. 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
claude mcp add --transport http plotwork http://plotwork.internal:3106/mcp \ --header "Authorization: Bearer $PLOTWORK_TOKEN"Cursor
Add this to ~/.cursor/mcp.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:
{ "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
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 | The tools, and how the local and hosted servers differ |
| Connecting an agent | Client settings for the hosted server and the npm package |
| API reference | Request and response shapes of the REST routes |
| Privacy policy | What the hosted service collects, none of which applies to a self-hosted server |