n4nAI

Self-hosting LangGraph agents on your own servers

Step-by-step guide to self-hosting LangGraph agents on your own infrastructure, from dependency isolation to production-grade deployment and observability.

n4n Team3 min read663 words

Audio narration

Coming soon — every post will get a voice note here.

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.

Tagslanggraphself-hostingagent-deploymentopen-source

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All agent deployment & hosting infrastructure posts →