Build an AI Agent: Python, Docker & Deployment Guide

How to Build an AI Agent: A Practical Developer’s Guide

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.

If you’ve spent any time on developer Twitter or Hacker News in the last year, you’ve seen the term “AI agent” thrown around constantly. Most of the explanations are either too abstract (“it’s software that thinks!”) or too shallow (a single API call wrapped in a for-loop). This guide skips the hype and shows you how to build an AI agent that actually does something useful, then run it in production like any other service in your stack — containerized, monitored, and secured.

We’ll build a real agent in Python, containerize it with Docker, and deploy it to a VPS. By the end, you’ll understand the architecture well enough to build your own agents for DevOps automation, log triage, or customer support.

What Is an AI Agent, Really?

An AI agent is a program that uses a large language model (LLM) as its reasoning engine, but unlike a chatbot, it doesn’t just generate text — it takes actions. It calls functions, hits APIs, reads files, queries databases, and uses the results to decide what to do next. The defining trait is the loop: observe, reason, act, repeat — until the task is done or a stopping condition is hit.

Agents vs. Simple Chatbots

A chatbot answers one prompt and stops. An agent:

  • Breaks a goal into sub-tasks
  • Chooses which tool to call for each sub-task (a shell command, an API, a search query)
  • Evaluates the tool’s output and decides the next step
  • Keeps state across multiple steps until the goal is satisfied
  • If you’ve used GitHub’s Copilot Workspace or any coding assistant that can run commands and fix its own errors, you’ve already used an agent. The same pattern applies to server automation, incident response, and content pipelines — which is exactly the kind of workload this site’s readers deal with daily.

    Core Components of an AI Agent

    Every agent, regardless of framework, is built from the same four pieces:

  • A model — the LLM that does the reasoning (GPT-4, Claude, Llama, etc.)
  • A tool set — functions the agent is allowed to call (shell exec, HTTP requests, DB queries)
  • Memory — short-term context for the current task, and optionally long-term storage for prior runs
  • A control loop — the code that ties it all together and decides when to stop
  • The Reasoning Loop

    The control loop is the part most tutorials gloss over. In practice it’s a simple while loop: send the model the current state, parse its response for a tool call, execute that tool, feed the result back in, repeat. The loop terminates when the model returns a final answer instead of a tool call, or when you hit a max-iteration safety limit (always set one — an ungoverned loop can burn through API credits fast).

    Tools and Function Calling

    Modern LLM APIs support structured “function calling,” where you describe available functions in JSON Schema and the model returns a structured call instead of free text. This is far more reliable than parsing natural language for commands.

    Memory and State

    For short tasks, an in-memory list of messages is enough. For anything long-running — like an agent that monitors your infrastructure over days — you’ll want persistent storage. Redis works well for this: fast, simple key-value storage for conversation state and tool results.

    Building Your First AI Agent in Python

    Here’s a minimal but functional agent that can execute shell commands and read files — useful as a base for a DevOps automation bot. It uses OpenAI’s function-calling API, but the same pattern works with Anthropic’s Claude API or any local model server.

    import json
    import subprocess
    from openai import OpenAI
    
    client = OpenAI()
    
    tools = [
        {
            "type": "function",
            "function": {
                "name": "run_shell_command",
                "description": "Run a read-only shell command and return its output",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "command": {"type": "string"}
                    },
                    "required": ["command"]
                }
            }
        }
    ]
    
    ALLOWED_COMMANDS = ("df", "uptime", "free", "docker ps", "systemctl status")
    
    def run_shell_command(command: str) -> str:
        if not command.startswith(ALLOWED_COMMANDS):
            return "Error: command not permitted by allowlist"
        result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=10)
        return result.stdout or result.stderr
    
    def run_agent(user_goal: str, max_steps: int = 6):
        messages = [{"role": "user", "content": user_goal}]
    
        for _ in range(max_steps):
            response = client.chat.completions.create(
                model="gpt-4o",
                messages=messages,
                tools=tools,
            )
            message = response.choices[0].message
            messages.append(message)
    
            if not message.tool_calls:
                return message.content  # final answer
    
            for call in message.tool_calls:
                args = json.loads(call.function.arguments)
                output = run_shell_command(args["command"])
                messages.append({
                    "role": "tool",
                    "tool_call_id": call.id,
                    "content": output,
                })
    
        return "Max steps reached without a final answer."
    
    if __name__ == "__main__":
        print(run_agent("Check disk usage and container status, summarize any issues"))

    Note the ALLOWED_COMMANDS allowlist. This is not optional — an agent with unrestricted shell access is a remote code execution vulnerability wearing a trench coat. Always constrain what tools your agent can actually invoke.

    If you’d rather not hand-roll the loop, frameworks like LangChain or LlamaIndex handle tool orchestration, retries, and memory for you, at the cost of extra abstraction layers you’ll eventually need to peel back to debug anything.

    Deploying Your AI Agent with Docker

    Once the agent works locally, containerize it so it runs identically in staging and production — the same reasoning we cover in our Docker Compose guide for multi-container apps applies here.

    Containerizing the Agent

    FROM python:3.12-slim
    
    WORKDIR /app
    
    COPY requirements.txt .
    RUN pip install --no-cache-dir -r requirements.txt
    
    COPY agent.py .
    
    # Never bake API keys into the image — inject at runtime
    ENV OPENAI_API_KEY=""
    
    USER nobody
    CMD ["python", "agent.py"]

    Build and run it with the key injected as an environment variable, not hardcoded:

    docker build -t my-ai-agent .
    docker run --rm 
      -e OPENAI_API_KEY="$OPENAI_API_KEY" 
      --memory=512m 
      --cpus=1 
      my-ai-agent

    The --memory and --cpus flags matter more for agents than for typical web services — a runaway reasoning loop can spike CPU usage if your max-step guard fails, so hard resource limits are cheap insurance.

    Running as a Persistent Service

    For an agent that needs to stay alive and respond to triggers (webhooks, cron, a message queue), wrap it in a small FastAPI service instead of a one-shot script, and run it under docker-compose alongside Redis for state:

    services:
      agent:
        build: .
        restart: unless-stopped
        environment:
          - OPENAI_API_KEY=${OPENAI_API_KEY}
          - REDIS_URL=redis://redis:6379
        depends_on:
          - redis
    
      redis:
        image: redis:7-alpine
        volumes:
          - redis-data:/data
    
    volumes:
      redis-data:

    Hosting Your AI Agent in Production

    An agent that calls external tools and possibly touches production infrastructure deserves a properly isolated VPS, not a shared dev box. A basic 2-vCPU / 4GB droplet from DigitalOcean is enough for most single-agent workloads, and their managed Redis add-on saves you from running your own. If you’re optimizing for cost-per-vCPU on long-running background workers, Hetzner cloud instances are consistently cheaper for the same specs — a detail we go into in our VPS cost comparison guide.

    Once deployed, you need visibility into whether the agent is actually alive and completing tasks, not silently stuck in a retry loop. BetterStack uptime and log monitoring can alert you the moment your agent’s health-check endpoint stops responding, which matters a lot more for autonomous services than for a static website — nobody’s refreshing a page to notice an agent has hung.

    Securing Your AI Agent

    Agents with tool access are a bigger attack surface than a typical CRUD app, because a cleverly crafted input (prompt injection) can trick the model into calling a tool it shouldn’t. Bake these controls in from day one:

  • Allowlist tools and commands explicitly — never let the model construct arbitrary shell strings
  • Run the container as a non-root user with no unnecessary filesystem access
  • Set hard timeouts and max-iteration limits on the reasoning loop
  • Log every tool call and its output so you can audit what the agent actually did
  • Keep API keys in environment variables or a secrets manager, never in the image or source control
  • Rate-limit external API calls the agent can trigger to avoid runaway cost or abuse
  • If your agent is public-facing (say, behind a webhook), put it behind Cloudflare for DDoS protection and WAF rules before traffic reaches your container — this is standard practice for any exposed endpoint, agent or not.

    Wrapping Up

    Building an AI agent isn’t fundamentally different from building any other backend service: you still need containerization, resource limits, monitoring, and a security model. The LLM is just the decision-making component in the middle. Start small — a single-tool agent solving one narrow problem — before reaching for multi-agent frameworks or long-running autonomous systems. Get the loop, the tool allowlist, and the deployment pipeline right first; complexity can come later once you trust the foundation.

    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

    Do I need a framework like LangChain to build an AI agent?
    No. For a single-purpose agent with a handful of tools, a plain function-calling loop (as shown above) is easier to debug and has fewer moving parts. Frameworks earn their keep once you’re managing many agents, complex memory, or multi-step planning across sessions.

    Which LLM should I use for an agent?
    GPT-4o and Claude models both support reliable function calling and handle multi-step reasoning well. For cost-sensitive, high-volume agents, smaller or open-source models (Llama 3, Mistral) can work if the tool set is narrow and well-defined — test accuracy on your specific tasks before committing.

    How do I stop an agent from running forever or looping?
    Always cap iterations with a max_steps counter and enforce per-call timeouts on any tool the agent invokes. Combine this with container-level CPU and memory limits so a stuck loop can’t consume unbounded resources.

    Is it safe to let an agent run shell commands on my server?
    Only with a strict allowlist of specific commands, run as a non-root, non-privileged user, ideally inside an isolated container with no access to secrets it doesn’t need. Never give an agent unrestricted shell=True execution against production.

    Can I run an AI agent without paying for a cloud LLM API?
    Yes — self-hosted models via Ollama or a GPU VPS can replace the OpenAI/Anthropic API for many agent workloads, at the cost of managing your own inference infrastructure and generally lower reasoning quality on complex tool-use tasks.

    How much does it cost to run an AI agent in production?
    Costs split into two buckets: LLM API usage (billed per token, scales with loop iterations) and hosting (a small VPS is usually $10-40/month). Set hard iteration limits and log token usage per run so costs stay predictable as usage grows.

    Comments

    Leave a Reply

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