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
- A Linux VM (Ubuntu 22.04 or Debian 12 is fine) with at least 4 GB RAM. 16 GB is recommended for any serious workload.
- Docker Engine and the
docker composeplugin. - Either an OpenRouter API key (cloud models) or a local Ollama running on the same machine (local models). See local-LLM agents for the Ollama variant.
- An hour, mostly for the first
docker compose upto pull images.
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:
- CPU. Most of the work is waiting on the model. 2 vCPUs is fine for low concurrency; 4-8 once you have several concurrent sessions.
- RAM. Postgres + three services + a few sandbox containers will fit comfortably in 4 GB at idle. Load grows with concurrent sessions; sandbox containers can be sized in
docker-compose.yml. - Disk. The event log lives in Postgres. 50 GB is plenty to start. Logs and Docker images are the other consumers.
- GPU. Only relevant if you run Ollama on the same box. For cloud models, no.
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:
LINCHPIN_API_KEY— the bearer token your clients will use to call the Linchpin API. Pick something long and random.openssl rand -base64 32will do.VAULT_ENCRYPTION_KEY— the Fernet key for the credential vault. Generate withpython3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())". Lose this key, lose every stored credential. Treat it like a database password.- One of:
MODEL_PROVIDER=openrouterandOPENROUTER_API_KEY=sk-or-...for cloud models.MODEL_PROVIDER=ollamaandOLLAMA_HOST=http://host.docker.internal:11434for local models.
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
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:
- Reverse proxy (Caddy, Nginx) with TLS in front of
:8000and:5173. - Auth at the proxy if you want anything stricter than the bearer-token check Linchpin ships.
- Back up the Postgres volume. The event log is your audit record; losing it loses session history.
- Restrict outbound network on sandbox containers if your agent tools should not reach the internet — set
network: "linchpin-none"when creating the environment. - Monitor disk on the Postgres volume; the event log grows.
Further reading
- Self-hosted agents — who picks this layer and why
- Local-LLM agents with Ollama — same guide, with Ollama instead of OpenRouter
- Architecture — what each service does and how they talk
- FAQ