Running self-hosting LangGraph agents inside your own VPC cuts third-party data exposure and gives you full control over scaling and retries. This guide walks through a reproducible path from a bare Linux box to a supervised agent process that survives reboots and reports its own health.
Step 1: Prepare the host and isolate dependencies
Start with a clean Ubuntu 22.04 instance. Create a dedicated unprivileged user and a Python virtual environment so your agent’s dependencies never collide with system packages.
sudo useradd -m -s /bin/bash agent
sudo -u agent python3 -m venv /opt/agent/venv
sudo -u agent /opt/agent/venv/bin/pip install --upgrade pip
Self-hosting LangGraph agents requires environment isolation because LangGraph pulls in langchain-core, async transports, and often conflicting transitive deps. A venv is enough for single-node deployments; for multi-tenant hosts, skip to Step 4 and containerize instead.
Install the base libraries immediately so later steps have a known-good foundation:
sudo -u agent /opt/agent/venv/bin/pip install \
langgraph==0.2.20 \
langchain-openai==0.1.7 \
fastapi==0.110.0 \
uvicorn==0.29.0
Step 2: Define a minimal LangGraph agent
Write the graph logic before worrying about transport. Below is a stateful reactor with one model call node and a conditional edge. It compiles to an app you can invoke synchronously.
# agent/graph.py
from typing import TypedDict, Annotated
import operator
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
class AgentState(TypedDict):
messages: Annotated[list, operator.add]
def call_model(state: AgentState):
model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
response = model.invoke(state["messages"])
return {"messages": [response]}
def should_continue(state: AgentState):
# Stop after one hop in this minimal example
return END
def build_graph():
g = StateGraph(AgentState)
g.add_node("agent", call_model)
g.set_entry_point("agent")
g.add_conditional_edges("agent", should_continue)
return g.compile()
This skeleton is intentionally dumb. Real agents add tool nodes, retry boundaries, and human-in-the-loop interrupts. The point is that the orchestration logic is plain Python and fully under your control.
Step 3: Configure model access without vendor lock-in
LangGraph’s ChatOpenAI accepts a base_url, so you can target any OpenAI-compatible server. When self-hosting LangGraph agents across multiple model providers, avoid hardcoding keys for each vendor in your image.
Point the client at a gateway that aggregates providers:
model = ChatOpenAI(
model="anthropic/claude-3-haiku",
base_url="https://api.n4n.ai/v1",
api_key=os.environ["GATEWAY_KEY"],
temperature=0,
)
One such option, n4n.ai, offers a single OpenAI-compatible endpoint covering 240+ models and performs automatic fallback when a provider is rate-limited or degraded, while honoring client routing directives and forwarding cache-control hints. That keeps your agent code unchanged if you shift models or providers. If you run your own vLLM instance, set base_url to its local address instead.
Step 4: Containerize for reproducible self-hosting
A Dockerfile makes the deployment artifact immutable. This matters when self-hosting LangGraph agents across staging and production.
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY agent/ ./agent/
COPY api.py .
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "api:app", "--host", "0.0.0.0", "--port", "8000"]
requirements.txt should pin the same versions from Step 1. Build and run:
docker build -t langgraph-agent:0.1 .
docker run -d -p 8000:8000 -e GATEWAY_KEY=$KEY langgraph-agent:0.1
Step 5: Run under systemd for single-node reliability
If you are not using Kubernetes, systemd is the most robust supervisor available on Linux. Create a unit file for the container or the venv directly.
# /etc/systemd/system/langgraph-agent.service
[Unit]
Description=LangGraph Agent
After=network.target
[Service]
User=agent
WorkingDirectory=/opt/agent
Environment=GATEWAY_KEY=your-key-here
ExecStart=/opt/agent/venv/bin/python -m uvicorn api:app --host 127.0.0.1 --port 8000
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now langgraph-agent
Restart=always covers crashes and uncaught exceptions. Watch logs with journalctl -u langgraph-agent -f.
Step 6: Wrap the agent in a FastAPI service
Expose the compiled graph over HTTP. Keep the API surface small and explicit.
# api.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from agent.graph import build_graph
import os
app = FastAPI()
graph = build_graph()
class Query(BaseModel):
input: str
@app.post("/invoke")
def invoke(q: Query):
try:
result = graph.invoke({"messages": [("user", q.input)]})
return {"output": result["messages"][-1].content}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/health")
def health():
return {"status": "ok"}
Bind to localhost behind a reverse proxy (Caddy or nginx) that terminates TLS and enforces auth. Do not expose /invoke directly to the internet without an API key check.
Step 7: Add health checks and structured logging
LangGraph does not ship with logging config. Add structured logs so you can trace each node execution in production.
import logging
import json
class JsonFormatter(logging.Formatter):
def format(self, record):
return json.dumps({
"ts": self.formatTime(record),
"level": record.levelname,
"msg": record.getMessage(),
})
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("agent")
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logger.addHandler(handler)
Emit a log line at the start of call_model with the model name and input length. The /health endpoint plus these logs give you enough signal to alert on stagnation or error spikes.
Step 8: Verify the deployment end to end
Confirm the service is reachable and the graph executes. From a machine with network access to the host:
curl -s -X POST http://localhost:8000/invoke \
-H "Content-Type: application/json" \
-d '{"input": "What is 2+2? Reply with just the number."}'
Expected success response:
{"output": "4"}
If you get a 500, check journalctl or container logs for the exception. A passing /health but failing /invoke usually means the model gateway is unreachable or the API key is missing.
For a more thorough check, run a loop that sends ten concurrent requests and asserts all return 200. That validates that self-hosting LangGraph agents under your supervision handles real traffic without thread exhaustion in the venv or container.
Operational notes
Once the agent is up, tune the worker count. Uvicorn defaults to a single worker; set --workers 4 on a 2-core box. If you outgrow one node, push the same image into a Kubernetes Deployment with a readiness probe hitting /health. The graph code does not change.
Self-hosting LangGraph agents is less about the framework and more about the boring parts: dependency pinning, process supervision, and log shipping. Get those right and the agent scales with your infrastructure instead of fighting it.