← Back to DevBytes

Self-Hosting AI Agents: Docker, VPS, and Cost Breakdown

What Is Self-Hosting AI Agents?

Self-hosting AI agents means running autonomous or semi-autonomous AI-powered software on infrastructure you controlβ€”typically a Virtual Private Server (VPS) or a dedicated machineβ€”rather than relying on managed cloud services or third-party platforms. These agents can perform tasks like research, code generation, data analysis, email drafting, or even orchestrating complex workflows across multiple tools. When you self-host, you deploy the agent runtime, its dependencies, and often a local language model or an API proxy inside Docker containers on a Linux VPS. You own the data, control the environment, and pay only for the compute and bandwidth you actually consume.

A typical self-hosted AI agent stack looks like this:

Why Self-Hosting Matters

1. Data Sovereignty and Privacy

When you use hosted AI agent platforms, your prompts, tool outputs, and intermediate reasoning steps often pass through third-party servers. For legal, healthcare, financial, or proprietary engineering work, this is a non-starter. Self-hosting keeps all sensitive context inside your own infrastructure. You can even run fully offline with local models like Llama 3, Mistral, or Qwen, ensuring zero data exfiltration.

2. Predictable Costs at Scale

Managed AI platforms charge per query, per agent-hour, or impose seat-based licensing that grows unpredictably. A VPS with a fixed monthly cost of $15–$60 can handle hundreds of thousands of agent iterations if architected correctly. Once you outgrow a single VPS, you can horizontally scale specific components (vector DB, message queue, GPU workers) rather than paying linearly for every conversation.

3. Full Control Over the Stack

You choose the model version, the timeout limits, the retry logic, the logging strategy, and the security boundaries. No vendor deprecates your workflow engine without notice. You can pin exact model versions and swap between providers or local models at will.

4. Customization Without Limits

Self-hosting lets you modify every layer: inject custom middleware for audit logging, add specialized tools that call internal APIs, implement fine-grained rate limiting per user, or integrate with legacy on-prem systems that a SaaS platform could never reach.

Architectural Overview

Before diving into Docker configurations and VPS setup, let's establish a reference architecture that balances simplicity with production readiness:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Internet / Users                           β”‚
β”‚  (Web dashboard, API clients, MCP clients)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚ HTTPS :443
               β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Caddy / Nginx Reverse Proxy β”‚
β”‚  - TLS termination (Let's    β”‚
β”‚    Encrypt auto-renewal)     β”‚
β”‚  - OAuth2-Proxy / Authelia   β”‚
β”‚  - Rate limiting             β”‚
β”‚  - WebSocket upgrades        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β–Ό          β–Ό          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Agent  β”‚ β”‚ LLM    β”‚ β”‚ Vector   β”‚
β”‚ Runtimeβ”‚ β”‚ Proxy  β”‚ β”‚ Store    β”‚
β”‚ (FastAPIβ”‚ (LiteLLMβ”‚ (ChromaDBβ”‚
β”‚  or     β”‚  or     β”‚  or       β”‚
β”‚  Crew)  β”‚  Ollama)β”‚  Qdrant)  β”‚
β””β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
    β”‚                      β”‚
    β–Ό                      β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Shared Persistent Storage      β”‚
β”‚  (Docker volumes / NFS mount)   β”‚
β”‚  - Agent logs & checkpoints     β”‚
β”‚  - Vector embeddings            β”‚
β”‚  - Model weights (if local)     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

This architecture isolates concerns into separate containers that can be independently scaled, upgraded, or moved to different hardware. The reverse proxy is the only component exposed to the public internet.

Step-by-Step Implementation

Step 1: Provision Your VPS

Choose a VPS provider based on your workload. Here are solid options as of mid-2025:

For this tutorial, we'll target a Hetzner CX42 (4 vCPU Intel/AMD, 8GB RAM, 80GB NVMe SSD, ~€12/month) running Ubuntu 24.04 LTS. This gives enough headroom to run the agent runtime, a small local model via Ollama, ChromaDB, and the reverse proxy simultaneously.

Once your VPS is provisioned, SSH in and perform initial hardening:

# Update system
sudo apt update && sudo apt upgrade -y

# Install Docker and Docker Compose plugin
sudo apt install -y docker.io docker-compose-v2

# Add your user to the docker group (logout and back in after)
sudo usermod -aG docker $USER

# Enable unattended security updates
sudo apt install -y unattended-upgrades
sudo dpkg-reconfigure --priority=low unattended-upgrades

# Set up a basic firewall β€” only allow SSH (22), HTTP (80), HTTPS (443)
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

# Verify Docker is running
docker --version
docker compose version

Step 2: Create the Project Structure

On your VPS, create a well-organized directory layout:

mkdir -p ~/ai-agent-stack/{agent-runtime,llm-proxy,vector-store,caddy,scripts,data}
cd ~/ai-agent-stack

This keeps each service isolated with its own configuration while sharing a single docker-compose.yml at the root level.

Step 3: The Docker Compose File β€” The Heart of the Stack

Create ~/ai-agent-stack/docker-compose.yml with the following content. This file defines every service, their networking, volumes, and environment variables:

# docker-compose.yml β€” Self-Hosted AI Agent Stack
# Place in ~/ai-agent-stack/

version: "3.9"

services:
  # ─── Reverse Proxy with auto-TLS ───────────────────────────────
  caddy:
    image: caddy:2.8-alpine
    container_name: caddy-proxy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
      - caddy_config:/config
    networks:
      - agent-net
    environment:
      - ACME_AGREE=true
    depends_on:
      - agent-runtime

  # ─── Agent Runtime (FastAPI-based) ────────────────────────────
  agent-runtime:
    build:
      context: ./agent-runtime
      dockerfile: Dockerfile
    container_name: agent-runtime
    restart: unless-stopped
    expose:
      - "8000"
    volumes:
      - ./data/agent-logs:/app/logs
      - ./data/agent-checkpoints:/app/checkpoints
      # Mount host Docker socket for sandboxed code execution
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      - LLM_PROXY_URL=http://llm-proxy:4000
      - VECTOR_STORE_URL=http://vector-store:8001
      - AGENT_LOG_LEVEL=INFO
      - AGENT_MAX_ITERATIONS=50
      - AGENT_TIMEOUT_SECONDS=300
      - OPENAI_API_KEY=${OPENAI_API_KEY:-}  # Pass through if using cloud LLM
      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
    networks:
      - agent-net
    depends_on:
      llm-proxy:
        condition: service_healthy
      vector-store:
        condition: service_started

  # ─── LLM Proxy (LiteLLM or Ollama) ────────────────────────────
  llm-proxy:
    image: ghcr.io/berriai/litellm:latest
    container_name: llm-proxy
    restart: unless-stopped
    expose:
      - "4000"
    volumes:
      - ./llm-proxy/config.yaml:/app/config.yaml:ro
      - llm_cache:/app/cache
    environment:
      - LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY:-sk-proxy-master-key}
      - LITELLM_SALT_KEY=${LITELLM_SALT_KEY:-salt-key-change-me}
    networks:
      - agent-net
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:4000/health"]
      interval: 30s
      timeout: 10s
      retries: 5

  # ─── Vector Store (ChromaDB) ──────────────────────────────────
  vector-store:
    image: chromadb/chroma:latest
    container_name: vector-store
    restart: unless-stopped
    expose:
      - "8001"
    volumes:
      - vector_data:/chroma/chroma
    environment:
      - IS_PERSISTENT=TRUE
      - ANONYMIZED_TELEMETRY=FALSE
      - ALLOW_RESET=TRUE
    networks:
      - agent-net

volumes:
  caddy_data:
  caddy_config:
  llm_cache:
  vector_data:

networks:
  agent-net:
    driver: bridge

Step 4: Configure the LLM Proxy

Create ~/ai-agent-stack/llm-proxy/config.yaml. LiteLLM acts as a unified gateway: it accepts OpenAI-compatible requests and routes them to multiple backendsβ€”local Ollama instances, cloud APIs, or even Azure/Vertex AIβ€”with built-in rate limiting, cost tracking, and fallback logic:

# llm-proxy/config.yaml β€” LiteLLM Gateway Configuration

general_settings:
  master_key: ${LITELLM_MASTER_KEY}
  database_connection_pool_size: 5
  allowed_routes: ["openai", "anthropic", "azure", "ollama"]

model_list:
  # Cloud fallback model β€” used when local model is overloaded or down
  - model_name: "gpt-4o-mini"
    litellm_params:
      model: "gpt-4o-mini"
      api_key: ${OPENAI_API_KEY}
      rpm: 500          # Requests per minute limit
      tpm: 200000       # Tokens per minute limit

  # Anthropic Claude for complex reasoning tasks
  - model_name: "claude-3-5-sonnet"
    litellm_params:
      model: "claude-3-5-sonnet-20240620"
      api_key: ${ANTHROPIC_API_KEY}
      rpm: 100
      tpm: 100000

  # Local Ollama model β€” primary workhorse, zero egress cost
  - model_name: "llama3-local"
    litellm_params:
      model: "ollama/llama3.1:8b"
      api_base: "http://ollama-host:11434"
      rpm: 200
      tpm: 50000
      # Higher timeout for local inference
      timeout: 120

litellm_settings:
  fallbacks:
    - from_model: "llama3-local"
      to_models: ["gpt-4o-mini"]
      fallback_mode: "completion"  # Auto-fallback on failure/timeout
  cache: true
  cache_type: "redis"
  cache_host: "redis-host"  # Optional: add Redis for caching LLM responses
  cache_ttl: 3600

router_settings:
  routing_strategy: "latency-based-routing"
  allowed_fails: 3
  num_retries: 2
  retry_after: 5

If you prefer running a local model directly without the proxy complexity, you can replace the llm-proxy service with Ollama itself:

# Alternative: Add to docker-compose.yml if running Ollama directly
ollama:
  image: ollama/ollama:latest
  container_name: ollama
  restart: unless-stopped
  expose:
    - "11434"
  volumes:
    - ollama_models:/root/.ollama
  networks:
    - agent-net
  # Optional: enable GPU if your VPS has one
  # deploy:
  #   resources:
  #     reservations:
  #       devices:
  #         - driver: nvidia
  #           count: 1
  #           capabilities: [gpu]

# Then pull a model after first startup:
# docker exec -it ollama ollama pull llama3.1:8b

Step 5: Build the Agent Runtime

Create ~/ai-agent-stack/agent-runtime/Dockerfile:

# agent-runtime/Dockerfile
FROM python:3.12-slim

# Install system dependencies for Playwright and sandboxed execution
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    build-essential \
    libglib2.0-0 \
    libnss3 \
    libnspr4 \
    libdbus-1-3 \
    libatk1.0-0 \
    libatk-bridge2.0-0 \
    libcups2 \
    libdrm2 \
    libxkbcommon0 \
    libxcomposite1 \
    libxdamage1 \
    libxrandr2 \
    libgbm1 \
    libpango-1.0-0 \
    libcairo2 \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Install Python packages
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Install Playwright browsers (headless Chromium for web scraping agents)
RUN playwright install --with-deps chromium

COPY . .

EXPOSE 8000

# Run the FastAPI agent server
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

Create ~/ai-agent-stack/agent-runtime/requirements.txt:

# agent-runtime/requirements.txt
fastapi==0.115.0
uvicorn[standard]==0.30.6
httpx==0.27.2
langchain==0.3.10
langchain-community==0.3.10
langgraph==0.2.35
openai==1.55.0
chromadb==0.5.20
playwright==1.48.0
pydantic==2.10.0
python-dotenv==1.0.1
sqlite3-api==1.0.0

Create the main agent server file at ~/ai-agent-stack/agent-runtime/main.py:

# agent-runtime/main.py
"""
Self-Hosted AI Agent Runtime
Exposes a REST API for creating, running, and monitoring AI agents.
"""
import os
import uuid
import json
import logging
from datetime import datetime, timedelta
from typing import Optional, Dict, Any
from contextlib import asynccontextmanager

from fastapi import FastAPI, HTTPException, BackgroundTasks, Query
from fastapi.responses import JSONResponse, StreamingResponse
from pydantic import BaseModel, Field
import httpx

# LangChain / LangGraph imports
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.sqlite import SqliteSaver
from langchain_openai import ChatOpenAI
from langchain_community.tools.playwright.navigate import NavigateTool
from langchain_community.tools.playwright.extract_text import ExtractTextTool
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import FakeEmbeddings
import sqlite3

# ─── Configuration ──────────────────────────────────────────────
LLM_PROXY_URL = os.getenv("LLM_PROXY_URL", "http://llm-proxy:4000")
VECTOR_STORE_URL = os.getenv("VECTOR_STORE_URL", "http://vector-store:8001")
LOG_DIR = os.getenv("AGENT_LOG_DIR", "/app/logs")
CHECKPOINT_DIR = os.getenv("AGENT_CHECKPOINT_DIR", "/app/checkpoints")
MAX_ITERATIONS = int(os.getenv("AGENT_MAX_ITERATIONS", "50"))
TIMEOUT_SECONDS = int(os.getenv("AGENT_TIMEOUT_SECONDS", "300"))

# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("agent-runtime")

# ─── Lifespan: Initialize checkpoint store ─────────────────────
@asynccontextmanager
async def lifespan(app: FastAPI):
    os.makedirs(LOG_DIR, exist_ok=True)
    os.makedirs(CHECKPOINT_DIR, exist_ok=True)
    # SQLite checkpoint DB for LangGraph state persistence
    checkpoint_db = os.path.join(CHECKPOINT_DIR, "checkpoints.db")
    app.state.checkpoint_db = checkpoint_db
    logger.info(f"Agent runtime started β€” checkpoints at {checkpoint_db}")
    yield
    logger.info("Agent runtime shutting down")

app = FastAPI(lifespan=lifespan, title="Self-Hosted AI Agent Runtime")

# ─── Pydantic Models ────────────────────────────────────────────
class AgentCreateRequest(BaseModel):
    task: str = Field(..., description="Natural language task description")
    model: str = Field(default="llama3-local", description="Model name (routed via LLM proxy)")
    max_iterations: int = Field(default=MAX_ITERATIONS)
    tools: list[str] = Field(default=["web_search", "code_executor"])
    system_prompt: Optional[str] = None

class AgentStatus(BaseModel):
    agent_id: str
    status: str  # "running", "completed", "failed", "timed_out"
    task: str
    created_at: str
    completed_at: Optional[str] = None
    iterations_used: int = 0
    final_output: Optional[str] = None
    error_message: Optional[str] = None

# ─── In-memory agent registry (backed by SQLite in production) ──
agent_registry: Dict[str, AgentStatus] = {}

# ─── API Endpoints ──────────────────────────────────────────────
@app.get("/health")
async def health_check():
    return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()}

@app.post("/agents", response_model=AgentStatus, status_code=201)
async def create_agent(request: AgentCreateRequest, background_tasks: BackgroundTasks):
    """Create a new agent task and begin execution asynchronously."""
    agent_id = str(uuid.uuid4())[:8]
    status = AgentStatus(
        agent_id=agent_id,
        status="running",
        task=request.task,
        created_at=datetime.utcnow().isoformat(),
    )
    agent_registry[agent_id] = status

    # Launch agent execution in background
    background_tasks.add_task(
        execute_agent,
        agent_id=agent_id,
        task=request.task,
        model=request.model,
        max_iterations=request.max_iterations,
        tools=request.tools,
        system_prompt=request.system_prompt,
    )
    logger.info(f"Agent {agent_id} created β€” task: {request.task[:80]}...")
    return status

@app.get("/agents/{agent_id}", response_model=AgentStatus)
async def get_agent_status(agent_id: str):
    """Poll agent status by ID."""
    if agent_id not in agent_registry:
        raise HTTPException(status_code=404, detail="Agent not found")
    return agent_registry[agent_id]

@app.get("/agents")
async def list_agents(
    status_filter: Optional[str] = Query(None),
    limit: int = Query(20, ge=1, le=100),
):
    """List all agents, optionally filtered by status."""
    agents = list(agent_registry.values())
    if status_filter:
        agents = [a for a in agents if a.status == status_filter]
    agents = sorted(agents, key=lambda a: a.created_at, reverse=True)[:limit]
    return {"agents": agents, "total": len(agent_registry)}

@app.delete("/agents/{agent_id}")
async def delete_agent(agent_id: str):
    """Remove an agent record from the registry."""
    if agent_id not in agent_registry:
        raise HTTPException(status_code=404, detail="Agent not found")
    del agent_registry[agent_id]
    return {"deleted": agent_id}

# ─── Agent Execution Logic ──────────────────────────────────────
async def execute_agent(
    agent_id: str,
    task: str,
    model: str,
    max_iterations: int,
    tools: list[str],
    system_prompt: Optional[str] = None,
):
    """Core agent loop using LangGraph with tool calling."""
    status = agent_registry[agent_id]
    start_time = datetime.utcnow()

    try:
        # Initialize the LLM via LiteLLM proxy (OpenAI-compatible endpoint)
        llm = ChatOpenAI(
            model=model,
            openai_api_key=os.getenv("OPENAI_API_KEY", "dummy"),
            openai_api_base=f"{LLM_PROXY_URL}/v1",
            temperature=0.7,
            max_tokens=4096,
            timeout=120,
        )

        # Build a simple StateGraph
        builder = StateGraph(dict)

        def agent_node(state: dict):
            """Core reasoning step β€” call LLM with tool definitions."""
            messages = state.get("messages", [])
            task_prompt = state.get("task", task)
            full_messages = [
                {"role": "system", "content": system_prompt or "You are a helpful AI assistant."},
                {"role": "user", "content": task_prompt},
            ] + messages

            # Bind tools (simplified β€” real implementation uses LangChain tools)
            llm_with_tools = llm.bind_tools([])  # Add actual tool definitions here
            response = llm_with_tools.invoke(full_messages)
            return {"messages": messages + [response.dict()], "iteration": state.get("iteration", 0) + 1}

        def should_continue(state: dict) -> str:
            iteration = state.get("iteration", 0)
            if iteration >= max_iterations:
                return END
            # Check for tool calls or final answer
            last_msg = state.get("messages", [{}])[-1]
            if last_msg.get("tool_calls"):
                return "tool_executor"
            return END

        def tool_executor(state: dict):
            """Execute tool calls from the LLM response."""
            messages = state.get("messages", [])
            last_msg = messages[-1] if messages else {}
            tool_results = []
            for tool_call in last_msg.get("tool_calls", []):
                # Simplified tool execution β€” real version dispatches to actual tools
                result = {"id": tool_call.get("id"), "output": f"Tool {tool_call.get('name')} executed"}
                tool_results.append(result)
            return {"messages": messages + tool_results}

        builder.add_node("agent", agent_node)
        builder.add_node("tool_executor", tool_executor)
        builder.set_entry_point("agent")
        builder.add_conditional_edges("agent", should_continue, {
            "tool_executor": "tool_executor",
            END: END,
        })
        builder.add_edge("tool_executor", "agent")

        # Compile with checkpoint saver for resilience
        checkpoint_db = os.path.join(CHECKPOINT_DIR, "checkpoints.db")
        with SqliteSaver.from_conn_string(checkpoint_db) as saver:
            graph = builder.compile(checkpointer=saver)
            config = {"configurable": {"thread_id": agent_id}}

            initial_state = {"task": task, "messages": [], "iteration": 0}
            result = graph.invoke(initial_state, config)

        # Success
        elapsed = (datetime.utcnow() - start_time).total_seconds()
        status.status = "completed"
        status.completed_at = datetime.utcnow().isoformat()
        status.iterations_used = result.get("iteration", 0)
        final_messages = result.get("messages", [])
        status.final_output = final_messages[-1].get("content", "") if final_messages else ""
        logger.info(f"Agent {agent_id} completed in {elapsed:.1f}s with {status.iterations_used} iterations")

    except Exception as e:
        elapsed = (datetime.utcnow() - start_time).total_seconds()
        status.status = "failed" if elapsed < TIMEOUT_SECONDS else "timed_out"
        status.completed_at = datetime.utcnow().isoformat()
        status.error_message = str(e)
        logger.error(f"Agent {agent_id} failed: {str(e)}")

# ─── Entrypoint ─────────────────────────────────────────────────
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

Step 6: Configure the Reverse Proxy (Caddy)

Create ~/ai-agent-stack/caddy/Caddyfile:

# caddy/Caddyfile β€” Reverse Proxy with Auto-TLS
# Replace YOUR_DOMAIN.com with your actual domain

YOUR_DOMAIN.com {
    # Let's Encrypt handles TLS certificate automatically
    
    # Security headers
    header {
        X-Content-Type-Options "nosniff"
        X-Frame-Options "DENY"
        X-XSS-Protection "1; mode=block"
        -Server
    }

    # Rate limit: 100 requests per second per IP
    rate_limit {
        zone agent_api {
            key {remote_host}
            events 100
            window 1s
        }
    }

    # Agent Runtime API
    handle /api/* {
        reverse_proxy agent-runtime:8000 {
            header_up X-Forwarded-For {remote_host}
            header_up X-Forwarded-Proto "https"
        }
    }

    # LLM Proxy (internal only β€” block external access)
    # Uncomment if you need direct LLM access from external clients:
    # handle /llm/* {
    #     reverse_proxy llm-proxy:4000
    # }

    # Default: serve a simple status page
    handle {
        respond / "Self-Hosted AI Agent Stack β€” Operational" 200
        respond /health "OK" 200
    }

    # Logging
    log {
        output file /var/log/caddy/access.log
        format json
    }
}

Step 7: Deploy the Stack

With all files in place, deploy everything from the project root:

cd ~/ai-agent-stack

# Create a .env file with secrets
cat > .env << 'EOF'
OPENAI_API_KEY=sk-your-openai-key-here
ANTHROPIC_API_KEY=sk-ant-your-anthropic-key-here
LITELLM_MASTER_KEY=sk-proxy-master-key-change-me
LITELLM_SALT_KEY=salt-key-change-me
EOF

# Secure the .env file
chmod 600 .env

# Build and start all services
docker compose up -d --build

# Check that all containers are healthy
docker compose ps

# View logs for debugging
docker compose logs -f agent-runtime

# Test the health endpoint
curl http://localhost:8000/health

# Create your first agent via the API
curl -X POST http://localhost:8000/agents \
  -H "Content-Type: application/json" \
  -d '{
    "task": "Research the latest developments in quantum computing and summarize the top 3 breakthroughs",
    "model": "llama3-local",
    "max_iterations": 20
  }'

# Poll for results (replace AGENT_ID with the returned ID)
curl http://localhost:8000/agents/AGENT_ID

Complete Cost Breakdown

Let's calculate the real monthly cost for a production-ready self-hosted AI agent stack serving a small team (up to 10 developers, ~50,000 agent iterations per month):

Base VPS Tier: CPU-Only Orchestration (No Local GPU)

Component Provider Monthly Cost
VPS (4 vCPU, 8GB RAM, 80GB NVMe) Hetzner CX42 €12.00
Block Storage (100GB for models/vectors) Hetzner Volume €4.40
Static IPv4 Address Included €0.00
Traffic (20TB included) Included €0.00
Infrastructure Subtotal €16.40

LLM API Costs (If Using Cloud Models for Fallback/Heavy Lifting)

Model Usage Estimate Monthly Cost
GPT-4o-mini (fallback, 200K tokens/day) 6M tokens/month @ $0.15/1M input ~$1.20
Claude 3.5 Sonnet (complex tasks, 50K tokens/day) 1.5M tokens/month @ $3/1M input ~$5.50
Local Llama 3.1 8B (primary, 500K tokens/day) 15M

β€” Ad β€”

Google AdSense will appear here after approval

← Back to all articles