Building AI Agents: A Practical DevOps Guide

Building AI Agents: A Developer’s Guide to Autonomous Workflows

Disclosure: This post contains one or more links to providers we have a real, registered affiliate/referral relationship with. We may earn a commission at no extra cost to you if you sign up through them.

Every few years a new abstraction shows up in software engineering that changes how we ship products. Containers did it for deployment. Kubernetes did it for orchestration. Now agentic AI is doing it for automation. If you’ve spent any time on engineering Twitter or Hacker News in the last year, you’ve seen the term thrown around constantly, but most of the content out there is either hand-wavy product marketing or academic papers you need a PhD to parse.

This guide skips both. We’re going to treat building AI agents like any other DevOps problem: define the architecture, write the code, containerize it, deploy it, and monitor it in production. By the end you’ll have a working agent running in Docker, with a clear path to scaling it on real infrastructure.

What Is an AI Agent, Actually?

An AI agent is a program that uses a large language model (LLM) not just to generate text, but to make decisions in a loop: observe the current state, decide on an action, execute that action (often by calling a tool or API), and then observe the result before deciding again. The key difference from a simple chatbot is autonomy — an agent can chain multiple steps together without a human clicking “send” after every message.

Agents vs. Simple LLM API Calls

A lot of people confuse “calling GPT-4 in a script” with “building an agent.” They’re not the same thing. A single API call is stateless: you send a prompt, you get a completion, done. An agent wraps that call in a loop with memory, tool access, and a stopping condition. Concretely, an agent architecture usually includes:

  • A planner that breaks a goal into steps
  • A tool layer that lets the model call functions (search the web, query a database, hit an API)
  • A memory store that persists context across steps (often a vector database)
  • An executor loop that keeps iterating until the goal is met or a max-step limit is hit
  • If your script doesn’t have at least a loop and a tool layer, you’ve built a prompt wrapper, not an agent. That distinction matters when you’re scoping infrastructure — agents are stateful, long-running processes, which changes how you deploy and monitor them compared to a stateless API endpoint.

    Why Self-Hosting Matters for Agent Workloads

    Most tutorials assume you’ll run your agent on a managed platform or a laptop. Neither works well in production. Managed agent platforms lock you into their pricing and rate limits, and a laptop obviously can’t run 24/7. If your agent needs to poll a queue, watch a filesystem, or run scheduled tasks, you need a real server.

    This is also where cost control becomes a real concern. LLM API calls aren’t free, and a poorly bounded agent loop can burn through your token budget in minutes if it gets stuck retrying a failed tool call. Self-hosting the orchestration layer — while still calling out to an LLM API — gives you full control over rate limiting, retry logic, and logging, none of which you get with a black-box SaaS agent builder.

    Building a Simple AI Agent in Python

    Let’s build something real. We’ll use the OpenAI API for the model calls and keep the orchestration logic in plain Python so you can see exactly what’s happening at each step — no heavy framework required to start.

    First, set up your environment:

    mkdir agent-demo && cd agent-demo
    python3 -m venv venv
    source venv/bin/activate
    pip install openai requests python-dotenv

    Now create agent.py:

    import os
    import json
    import requests
    from openai import OpenAI
    from dotenv import load_dotenv
    
    load_dotenv()
    client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
    
    def get_weather(city: str) -> str:
        """Tool: fetch current weather for a city."""
        resp = requests.get(f"https://wttr.in/{city}?format=3", timeout=5)
        return resp.text.strip()
    
    TOOLS = [
        {
            "type": "function",
            "function": {
                "name": "get_weather",
                "description": "Get current weather for a given city",
                "parameters": {
                    "type": "object",
                    "properties": {"city": {"type": "string"}},
                    "required": ["city"],
                },
            },
        }
    ]
    
    def run_agent(user_goal: str, max_steps: int = 5):
        messages = [{"role": "user", "content": user_goal}]
    
        for step in range(max_steps):
            response = client.chat.completions.create(
                model="gpt-4o-mini",
                messages=messages,
                tools=TOOLS,
            )
            message = response.choices[0].message
            messages.append(message)
    
            if not message.tool_calls:
                return message.content
    
            for call in message.tool_calls:
                args = json.loads(call.function.arguments)
                if call.function.name == "get_weather":
                    result = get_weather(args["city"])
                else:
                    result = "Unknown tool"
    
                messages.append({
                    "role": "tool",
                    "tool_call_id": call.id,
                    "content": result,
                })
    
        return "Max steps reached without a final answer."
    
    if __name__ == "__main__":
        print(run_agent("What's the weather like in Lisbon right now?"))

    This is the entire skeleton of an agent: a loop, a tool the model can call, and a hard stop at max_steps so a broken loop can’t run forever and rack up API charges. Every production agent framework — LangChain, CrewAI, AutoGen — is a more elaborate version of this same pattern. Understanding this loop first will save you hours of debugging once you move to a framework, because you’ll actually know what’s happening under the abstraction.

    Adding Memory with a Vector Store

    Once your agent needs to recall information across sessions, a plain message list isn’t enough. Most production agents pair the loop above with a vector database like Chroma or Pinecone to store and retrieve embeddings of past interactions or documents. The pattern is straightforward: embed incoming text, store it with metadata, and query the store for relevant context before each LLM call. We won’t build the full retrieval pipeline here, but keep in mind that memory is a separate concern from the agent loop — don’t couple them tightly, or you’ll struggle to swap vector stores later.

    Containerizing Your Agent with Docker

    Once the logic works locally, package it so it runs identically anywhere. If you’re new to multi-service setups, our guide to Docker Compose fundamentals covers the basics this section builds on.

    FROM python:3.12-slim
    
    WORKDIR /app
    
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    
    COPY agent.py .
    
    ENV PYTHONUNBUFFERED=1
    
    CMD ["python", "agent.py"]

    Running a Multi-Agent Stack with Docker Compose

    Real-world setups rarely involve a single agent. You’ll typically have an agent process, a vector database for memory, and maybe a Redis instance for task queuing. Here’s a docker-compose.yml that ties them together:

    version: "3.9"
    services:
      agent:
        build: .
        env_file: .env
        depends_on:
          - redis
          - chroma
        restart: unless-stopped
    
      redis:
        image: redis:7-alpine
        restart: unless-stopped
    
      chroma:
        image: chromadb/chroma:latest
        ports:
          - "8000:8000"
        volumes:
          - chroma-data:/chroma/chroma
        restart: unless-stopped
    
    volumes:
      chroma-data:

    Bring it up with docker compose up -d --build, and you have an isolated, reproducible agent stack that behaves the same on your laptop as it does on a production VPS. This matters more than it sounds — LLM agent behavior can be subtly sensitive to Python and library versions, and Docker eliminates the “works on my machine” class of bugs entirely.

    Monitoring and Scaling Agents in Production

    Agents fail differently than typical web services. Instead of a clean 500 error, an agent might silently loop, hallucinate a tool call that doesn’t exist, or quietly burn through your API budget over a weekend while nobody’s watching. Standard uptime monitoring isn’t enough here — you need to track step counts, token usage, and tool-call failure rates.

    Logging Every Step for Debuggability

    At minimum, log the full message history for every agent run, not just the final output. When an agent misbehaves, the failure is almost always buried three or four steps back in the transcript, not in the final response. Structure your logs as JSON so they’re easy to query later:

    import logging
    import json
    
    logger = logging.getLogger("agent")
    
    def log_step(step: int, message: dict):
        logger.info(json.dumps({"step": step, "message": message}))

    Ship those logs somewhere durable rather than relying on docker logs, which rotates and disappears. For teams that don’t want to run their own ELK stack, a hosted option like BetterStack gives you centralized log aggregation and uptime alerting without the operational overhead — genuinely useful once you have more than one agent running unattended. If you’re comparing observability options more broadly, see our breakdown of self-hosted monitoring stacks for the tradeoffs.

    Setting Hard Limits to Control Cost

    Beyond max_steps, add a token budget check before every LLM call and kill the run if it’s exceeded. This single guardrail has saved more than a few teams from a surprise five-figure API bill after an agent got stuck in a retry loop over a long weekend.

    Choosing Infrastructure for Your Agent Workloads

    Agents that run continuously — polling queues, watching webhooks, executing scheduled jobs — need a server, not a laptop or a serverless function with a 15-minute timeout. A small VPS is more than enough to start. DigitalOcean droplets are a solid default if you want a simple, well-documented control panel and predictable pricing while you’re iterating on the architecture. If you’re optimizing for cost once your agent stack stabilizes, Hetzner cloud instances offer significantly cheaper compute per dollar for the same workload, which adds up once you’re running multiple agents around the clock.

    Whichever provider you pick, harden the box before you deploy anything — our Linux server hardening checklist is a good starting point, since an agent with API keys and tool access on a poorly secured server is a bigger liability than a typical web app.

    Security Considerations for Autonomous Agents

    An agent that can execute code, hit arbitrary URLs, or write to a filesystem is functionally similar to giving a script root-ish access to your infrastructure. A few non-negotiables:

  • Never let an agent execute raw shell commands from model output without a strict allowlist of permitted operations
  • Scope API keys narrowly — a tool that only needs read access shouldn’t hold a key with write permissions
  • Sandbox any code-execution tools in a disposable container, not the host running your agent process
  • Rate-limit and log every outbound HTTP call the agent makes, since a compromised or confused agent can be tricked into exfiltrating data through a tool call
  • Treat all tool outputs as untrusted input — a malicious webpage or API response can attempt prompt injection against your own agent
  • That last point deserves emphasis: prompt injection through tool results is one of the most common real-world agent vulnerabilities, and it’s easy to overlook because it doesn’t look like a traditional security bug.

    Recommended: Ready to put this into practice? DigitalOcean is a tool we use for exactly this, and we have a real, disclosed affiliate relationship with them.

    FAQ

    What’s the difference between building AI agents and using a chatbot API?
    A chatbot API call is a single stateless request-response exchange. Building AI agents involves wrapping model calls in a loop with memory, tool access, and a decision-making process that can span multiple steps without human input at each stage.

    Do I need a framework like LangChain to build an agent?
    No. As shown in the Python example above, you can build a working agent loop with plain code and the OpenAI SDK. Frameworks add convenience for complex multi-agent orchestration, but understanding the raw loop first makes debugging framework-based agents much easier.

    How much does it cost to run an AI agent in production?
    Costs depend on token usage per step and how many steps your loop runs before stopping. A well-bounded agent with a hard max_steps limit and token budget checks typically costs a few cents to a few dollars per run, depending on the model. Unbounded loops are the main cause of runaway bills.

    Can I run AI agents entirely on my own hardware without cloud APIs?
    Yes, using local models served through tools like Ollama or vLLM instead of a hosted API. This removes per-token costs but requires GPU hardware and generally produces weaker reasoning than frontier hosted models, so it’s a tradeoff between cost and capability.

    Is Docker required to deploy an AI agent?
    Not strictly, but it’s strongly recommended. Agent behavior can be sensitive to Python and dependency versions, and Docker guarantees your local testing environment matches production exactly, which matters more for agents than for typical stateless services.

    How do I prevent an agent from getting stuck in an infinite loop?
    Always enforce a hard max_steps limit in your executor loop, plus a token budget check before each LLM call. Log every step so you can diagnose why a loop got stuck rather than just killing and restarting it blindly.

    Wrapping Up

    Building AI agents isn’t magic — it’s a loop, a tool layer, some memory, and a lot of the same DevOps discipline you’d apply to any other production service: containerize it, monitor it, log everything, and put hard limits on anything that costs money per call. Start with the plain Python loop above, containerize it with Docker once it works, and deploy it on a small VPS you actually control. That gets you further, faster, than reaching for a heavyweight framework before you understand what it’s abstracting away.

    Comments

    Leave a Reply

    Your email address will not be published. Required fields are marked *