Linchpin / Notes / Self-hosting on a single VM

Self-hosting AI agents on a single VM

A step-by-step deployment for Linchpin: one Linux VM, Docker, ~5 minutes of setup. You end with a working agent runtime on infrastructure you own and an SSE-streaming session you can curl.

Posted 2026-05-13 · By the Linchpin team · ~15 min read

This guide is for the case where you want to run AI agents on infrastructure you own, against a model of your choice, without paying a managed-agent vendor. The target topology is one Linux VM running Docker. Linchpin's three services and Postgres all live on that single box. Per-session sandbox containers spawn on the same host.

If you want the background on what an "agent runtime" is and where this layer sits, read what is a managed agent runtime? first.

What you will need

1. Provision the VM

Any provider works. The shape we want is a single machine, not a cluster — Linchpin is intentionally simple at small scale, and scaling to many sessions on one VM beats scaling to many VMs with one session each until you have real load to justify the complexity. Pick a region close to where your model provider lives (if you are using OpenRouter, latency-wise this matters less than you think).

Sizing notes:

2. Install Docker

Follow Docker's official install docs for your distribution — the engine plus the compose plugin. The short form on Ubuntu:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# log out / back in so the group change applies
docker compose version

The last command should print Docker Compose version v2.x. If it does not, install docker-compose-plugin manually.

3. Clone Linchpin

git clone https://github.com/linchpinhq/linchpin
cd linchpin

The repo is small (~3k LOC). You can read it top to bottom if you want to understand what you are about to run. The relevant files for this guide are docker-compose.yml, .env.example, and the per-service folders (linchpin-api/, linchpin-connector/, linchpin-console/).

4. Configure environment variables

cp .env.example .env

Open .env and set:

Security

Do not commit .env. The default .gitignore excludes it; double-check before you push your fork anywhere.

5. Bring it up

docker compose up --build

First run pulls images and builds the local services. Expect a few minutes. Subsequent runs are seconds. You should see:

# abbreviated
[+] Running 4/4
 ✔ Container postgres-1            Healthy
 ✔ Container linchpin-connector-1  Started
 ✔ Container linchpin-api-1        Started
 ✔ Container linchpin-console-1    Started

From here, the API is at http://<vm-ip>:8000 and the console at http://<vm-ip>:5173. For a production deploy, put both behind a reverse proxy with TLS and authentication at the edge — Linchpin does not bring its own TLS.

6. Create your first session

Three calls: define an environment, define an agent, open a session.

# env
KEY=$LINCHPIN_API_KEY
API=http://localhost:8000

# 1. environment
curl -sX POST $API/v1/environments \
  -H "Authorization: Bearer $KEY" \
  -d '{"id":"e1","network":"linchpin-open","tools":["bash","read","write","grep"]}'

# 2. agent
curl -sX POST $API/v1/agents \
  -H "Authorization: Bearer $KEY" \
  -d '{"id":"a1","model":"anthropic/claude-sonnet-4.6","system":"You are a helpful research agent."}'

# 3. session
SESSION=$(curl -sX POST $API/v1/sessions \
  -H "Authorization: Bearer $KEY" \
  -d '{"agent_id":"a1","environment_id":"e1"}' | jq -r .id)
echo $SESSION

7. Send a message, stream the reply

# send
curl -sX POST $API/v1/sessions/$SESSION/events \
  -H "Authorization: Bearer $KEY" \
  -d '{"type":"user.message","payload":{"text":"Say hi in 5 words"}}'

# stream
curl -N $API/v1/sessions/$SESSION/stream \
  -H "Authorization: Bearer $KEY"

You should see session.status_running, then agent.message_delta events with token chunks, then session.status_idle with usage stats. That is the loop.

What you have just deployed

Fig. 01 · What runs on the VM after step 5
flowchart LR
    client[Your client] -->|HTTP / SSE| api[linchpin-api]
    api --> pg[(Postgres 16)]
    api --> conn[linchpin-connector]
    conn -.->|docker exec| sbx[per-session
sandbox] api -->|model call| model[OpenRouter
or Ollama] style api fill:#B83A1A,color:#F4F2EC style pg fill:#1A1A1A,color:#F4F2EC

Hardening for production

This is the bare-metal happy path. To put it in front of real users:

Further reading