AI Phone Agent: Self-Host One with Docker & DevOps Tools

How to Build a Self-Hosted AI Phone Agent with Docker

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 shopped around for a hosted AI phone agent platform, you’ve probably noticed the per-minute pricing adds up fast once you’re handling real call volume. For developers and sysadmins who already run Docker infrastructure, self-hosting an AI phone agent is a realistic alternative — you control the stack, the data, and the cost.

This guide walks through the architecture, the Docker Compose stack, telephony integration, and deployment to a VPS, so you can run your own AI phone agent instead of paying per-minute SaaS fees.

What Is an AI Phone Agent?

An AI phone agent is a voice pipeline that answers or places phone calls, transcribes speech in real time, feeds that transcript to a language model, and speaks the model’s response back to the caller. Under the hood it’s four components glued together:

  • Telephony layer — receives/places PSTN calls (Twilio, SIP trunk, or Asterisk)
  • Speech-to-text (STT) — converts caller audio to text in near real time
  • LLM orchestration — generates a response, optionally calling tools/APIs
  • Text-to-speech (TTS) — converts the LLM’s reply back into audio
  • Each of these can be a hosted API or an open-source model running in a container. The self-hosted approach swaps API calls for local inference where it makes sense, and keeps everything else containerized for reproducibility.

    Why Self-Host Instead of Using a SaaS Platform

    The economics change quickly at volume. A SaaS AI phone agent platform typically charges $0.05–$0.15 per minute on top of telephony carrier costs. If you’re running a support line or an outbound campaign doing thousands of minutes a month, that’s real money. Self-hosting shifts the cost to a fixed VPS bill plus carrier minutes, and gives you full control over latency, logging, and model choice.

    The tradeoff is operational complexity — you’re now responsible for uptime, scaling, and monitoring, which is exactly the kind of work a DevOps-oriented reader is already comfortable with. If you haven’t containerized a networked service before, it’s worth reading our Docker networking basics guide first, since the telephony and STT containers need to talk to each other reliably.

    Core Architecture

    A minimal self-hosted stack looks like this:

    Caller ↔ SIP Trunk ↔ Asterisk (container) ↔ Audio bridge (WebSocket)
                                                       ↓
                                            STT service (Whisper, container)
                                                       ↓
                                            LLM orchestrator (container)
                                                       ↓
                                            TTS service (Piper/Coqui, container)
                                                       ↓
                                            Back to Asterisk → Caller

    Each box is its own container, connected over an internal Docker network. This separation matters operationally: you can scale STT and TTS independently, swap the LLM provider without touching telephony, and restart any single component without dropping the whole stack.

    Setting Up the Stack with Docker

    Start with a docker-compose.yml that defines the core services. This example uses Asterisk for SIP telephony, OpenAI’s Whisper for STT, a lightweight FastAPI orchestrator for the LLM logic, and Piper for TTS.

    version: "3.9"
    services:
      asterisk:
        image: andrius/asterisk:18
        ports:
          - "5060:5060/udp"
          - "10000-10100:10000-10100/udp"
        volumes:
          - ./asterisk-config:/etc/asterisk
        networks:
          - agent-net
    
      stt:
        build: ./stt
        environment:
          - MODEL_SIZE=small
        networks:
          - agent-net
    
      orchestrator:
        build: ./orchestrator
        environment:
          - LLM_API_KEY=${LLM_API_KEY}
        depends_on:
          - stt
          - tts
        networks:
          - agent-net
    
      tts:
        build: ./tts
        networks:
          - agent-net
    
    networks:
      agent-net:
        driver: bridge

    The orchestrator container is the piece you’ll customize most. A minimal version using FastAPI to bridge transcripts to an LLM might look like this:

    # orchestrator/main.py
    from fastapi import FastAPI, WebSocket
    import httpx
    import os
    
    app = FastAPI()
    LLM_API_KEY = os.environ["LLM_API_KEY"]
    
    @app.websocket("/call")
    async def handle_call(websocket: WebSocket):
        await websocket.accept()
        async with httpx.AsyncClient() as client:
            while True:
                transcript = await websocket.receive_text()
                response = await client.post(
                    "https://api.openai.com/v1/chat/completions",
                    headers={"Authorization": f"Bearer {LLM_API_KEY}"},
                    json={
                        "model": "gpt-4o-mini",
                        "messages": [{"role": "user", "content": transcript}],
                    },
                )
                reply = response.json()["choices"][0]["message"]["content"]
                await websocket.send_text(reply)

    This is deliberately minimal — no retry logic, no conversation memory — but it’s enough to prove the pipeline end-to-end before you add production concerns like call state, interruption handling, and logging.

    Connecting Telephony (Twilio or SIP Trunk)

    You have two realistic options for the telephony leg:

  • Twilio Media Streams — easiest to set up, no SIP trunk needed, pay-per-minute carrier cost only. Twilio streams raw audio to a WebSocket endpoint you control, which is a natural fit for the orchestrator above. See Twilio’s Media Streams docs for the exact WebSocket payload format.
  • Self-hosted Asterisk + SIP trunk — more setup work (NAT traversal, SIP trunk provider, dialplan config) but no per-vendor markup beyond the trunk provider’s per-minute rate, and full control over call routing.
  • For most teams starting out, Twilio Media Streams is the faster path — you skip SIP/NAT headaches entirely and can have a working prototype in an afternoon. Move to Asterisk once call volume justifies the added ops burden.

    Deploying to a VPS

    Audio streaming is latency-sensitive, so where you deploy matters. A few practical guidelines:

  • Pick a region close to your majority caller base — round-trip latency over 300ms causes audible lag in the conversation.
  • Use a VPS with at least 4 vCPUs / 8GB RAM if you’re running Whisper locally instead of an API; STT is the most CPU/GPU-hungry piece of the stack.
  • Open only the UDP ports Asterisk needs (SIP signaling + RTP range) and put everything else behind a private Docker network.
  • Terminate TLS/WSS at a reverse proxy in front of the orchestrator’s WebSocket endpoint — never expose it in plaintext.
  • If you haven’t hardened a fresh VPS before running production telephony workloads on it, check our VPS hardening checklist before opening the required ports to the internet.

    For the actual host, DigitalOcean droplets and Hetzner cloud servers are both solid picks — Hetzner tends to be cheaper per vCPU, DigitalOcean has slightly better network peering in North America. Either works fine for a stack this size; pick based on where your callers are.

    Monitoring and Scaling

    A phone agent that silently drops calls is worse than no phone agent at all, so monitoring isn’t optional here. At minimum, track:

  • Call setup success rate (SIP 200 OK vs failures)
  • STT latency per utterance
  • LLM response latency
  • TTS generation time
  • Container restart counts
  • BetterStack is a reasonable option for uptime and log monitoring across the whole stack — you can point it at your Docker container logs and get alerted the moment the orchestrator starts throwing errors, rather than finding out from an angry caller. Pair it with basic container health checks in your docker-compose.yml so Docker itself restarts a crashed STT or TTS container automatically.

    Once a single instance is stable, scaling out is mostly a matter of running multiple orchestrator + STT + TTS replicas behind a load balancer, with Asterisk (or Twilio) distributing calls across them. If you’re new to running Compose stacks in production, our Docker Compose production guide covers restart policies, health checks, and resource limits in more depth.

    If you’re serving the agent’s web-facing dashboard or webhook endpoints publicly, put Cloudflare in front of it for DDoS protection and TLS termination — telephony webhook endpoints are a common target for probing traffic once they’re discoverable.

    Handling Conversation State

    The minimal orchestrator above treats every transcript as a one-off request, which breaks down the moment a caller references something they said 20 seconds earlier. In production you’ll want:

  • A per-call conversation buffer keyed by call SID, stored in Redis with a short TTL
  • A system prompt that includes the last N turns, not just the current utterance
  • A hard timeout that ends the call gracefully if the LLM doesn’t respond within ~2 seconds, falling back to a canned “let me check on that” filler phrase to avoid dead air
  • Dead air is the single biggest giveaway that a caller is talking to a bot rather than a human, so latency budgeting deserves more attention than the LLM prompt itself.

    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

    Q: Do I need a GPU to run an AI phone agent’s speech models?
    A: Not necessarily. Whisper’s smaller models run acceptably on CPU for low call volume, but if you’re handling more than a handful of concurrent calls, a GPU (or a hosted STT API) will keep latency down significantly.

    Q: Is Twilio required, or can I avoid it entirely?
    A: Twilio isn’t required — Asterisk with a SIP trunk provider gives you full self-hosted telephony — but Twilio’s Media Streams API is significantly faster to prototype with and worth using for an initial proof of concept.

    Q: How much does self-hosting actually save compared to a SaaS AI phone agent?
    A: It depends on volume. Below a few thousand minutes a month, a SaaS platform is often cheaper once you factor in your own engineering time. Above that, self-hosting on a $40–80/month VPS usually wins.

    Q: What’s the biggest source of latency in the pipeline?
    A: Usually STT transcription, followed by LLM response time. TTS is typically the fastest step if you’re using a lightweight model like Piper.

    Q: Can this stack handle outbound calls, not just inbound?
    A: Yes — both Asterisk and Twilio support originating calls. The orchestrator logic is identical; only the call-initiation step differs.

    Q: How do I keep call recordings compliant with regulations like TCPA or GDPR?
    A: Store recordings and transcripts encrypted at rest, log consent/disclosure at call start, and set a retention policy that automatically purges data after a defined period — this is a legal question as much as a technical one, so involve counsel for anything customer-facing.

    Self-hosting an AI phone agent isn’t a weekend project, but if you’re already comfortable with Docker and basic Linux ops, none of the individual pieces here are exotic. Start with Twilio Media Streams and a hosted LLM API to validate the concept, then move pieces in-house — Whisper, then TTS, then full SIP — as volume and cost justify it.

    Comments

    Leave a Reply

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