Agent Studio

Docker Deployment

Deploy Agent Studio using Docker and Docker Compose

Docker Deployment

Agent Studio provides Docker images for easy deployment. This guide covers both local development and production deployment with self-hosted LiveKit and SIP telephony.

Prerequisites

  • Docker 24.0+
  • Docker Compose 2.20+
  • 4GB RAM minimum (8GB recommended)
  • For SIP: Twilio account with Elastic SIP Trunking

Local Development

Quick Start

  1. Clone the repository:
git clone https://github.com/tap-health/agent-studio.git
cd agent-studio
  1. Copy environment file:
cp .env.example .env
  1. Start all services:
docker-compose -f docker/docker-compose.yml up -d
  1. Check service status:
docker-compose -f docker/docker-compose.yml ps

Services

The development stack includes:

ServicePortDescription
api8000FastAPI server with hot reload
worker-LiveKit voice worker
db5432PostgreSQL 16
redis6379Redis 7
livekit7880LiveKit server (dev mode)

The development LiveKit server runs in --dev mode which does not support SIP. For SIP telephony testing, use the production setup.

Accessing Services

Development Workflow

The API service mounts your source code and enables hot reload:

volumes:
  - ../src:/app/src:ro

Changes to Python files automatically restart the server.

Viewing Logs

# All services
docker-compose -f docker/docker-compose.yml logs -f

# Specific service
docker-compose -f docker/docker-compose.yml logs -f api

# Last 100 lines
docker-compose -f docker/docker-compose.yml logs --tail=100 api

Stopping Services

# Stop all
docker-compose -f docker/docker-compose.yml down

# Stop and remove volumes (reset data)
docker-compose -f docker/docker-compose.yml down -v

Production Deployment

Building the Image

The production Dockerfile uses multi-stage builds for smaller images:

docker build -t agent-studio:latest -f docker/Dockerfile .

Image features:

  • Multi-stage build (builder + runtime)
  • Non-root user
  • Health checks
  • Minimal dependencies

Environment Variables

Required environment variables for production:

# Database
DATABASE_URL=postgresql+asyncpg://user:pass@host:5432/dbname

# Redis
REDIS_URL=redis://host:6379/0

# Security (generate secure values!)
JWT_SECRET=your-secure-jwt-secret-min-32-chars
ENCRYPTION_KEY=your-secure-encryption-key-32chars

# LiveKit (self-hosted)
LIVEKIT_URL=ws://localhost:7880
LIVEKIT_API_KEY=your-api-key
LIVEKIT_API_SECRET=your-api-secret

# SIP Telephony (optional)
LIVEKIT_SIP_TRUNK_ID=your-outbound-trunk-id

Self-Hosted LiveKit with SIP

For production deployments with phone call capability, deploy LiveKit server and SIP service.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                      Your Infrastructure                         │
├─────────────────────────────────────────────────────────────────┤
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐        │
│  │  Caddy   │  │   API    │  │  Worker  │  │  Worker  │        │
│  │  :80/443 │  │  :8000   │  │    #1    │  │    #2    │        │
│  └──────────┘  └──────────┘  └──────────┘  └──────────┘        │
│                                    │                            │
│  ┌──────────┐  ┌──────────┐       │                            │
│  │ LiveKit  │◄─│   SIP    │◄──────┘                            │
│  │  :7880   │  │  :5060   │                                    │
│  └──────────┘  └──────────┘                                    │
│       │              │                                          │
│  ┌────┴────┐  ┌──────┴──────┐                                  │
│  │  Redis  │  │  PostgreSQL │                                  │
│  │  :6379  │  │    :5432    │                                  │
│  └─────────┘  └─────────────┘                                  │
└─────────────────────────────────────────────────────────────────┘

                       │ SIP/RTP

                    Twilio


                     PSTN

Docker Compose Configuration

Add these services to your docker-compose.prod.yml:

services:
  # ... existing services (api, worker, db, redis) ...

  # ==========================================================================
  # LiveKit Server (Self-hosted)
  # ==========================================================================
  livekit:
    image: livekit/livekit-server:latest
    network_mode: host
    environment:
      # Note: LIVEKIT_KEYS format requires space after colon: "key: secret"
      LIVEKIT_KEYS: "${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}"
      LIVEKIT_REDIS_ADDRESS: localhost:6379
      LIVEKIT_REDIS_PASSWORD: ${REDIS_PASSWORD}
    volumes:
      - ./livekit.yaml:/etc/livekit.yaml:ro
    command: --config /etc/livekit.yaml
    restart: unless-stopped

  # ==========================================================================
  # LiveKit SIP Server
  # ==========================================================================
  # Note: SIP server config file doesn't support env var substitution,
  # so we use an entrypoint with sed to replace ${REDIS_PASSWORD}
  sip:
    image: livekit/sip:latest
    network_mode: host
    environment:
      LIVEKIT_API_KEY: ${LIVEKIT_API_KEY}
      LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET}
      LIVEKIT_WS_URL: ws://localhost:7880
      REDIS_PASSWORD: ${REDIS_PASSWORD}
    volumes:
      - ./sip.yaml:/sip/config.template.yaml:ro
    entrypoint:
      [
        "/bin/sh",
        "-c",
        "sed \"s/\\$${REDIS_PASSWORD}/$REDIS_PASSWORD/g\" /sip/config.template.yaml > /sip/config.yaml && exec livekit-sip --config /sip/config.yaml",
      ]
    depends_on:
      - livekit
    restart: unless-stopped

SIP Configuration File

Create sip.yaml:

ws_url: ws://localhost:7880
sip:
  port: 5060
  rtp_port_start: 10000
  rtp_port_end: 20000
redis:
  address: localhost:6379
  password: ${REDIS_PASSWORD}
logging:
  level: info

The ${REDIS_PASSWORD} placeholder is replaced at runtime by the entrypoint script since the SIP server config doesn't support environment variable substitution natively.

LiveKit Configuration File

Create livekit.yaml:

port: 7880
rtc:
  port_range_start: 7882
  port_range_end: 7882
  tcp_port: 7881
  # IMPORTANT: Set node_ip to your VM's public IP for NAT traversal
  # Do not use use_external_ip: true as it can cause issues
  node_ip: <your-vm-public-ip>
  enable_loopback_candidate: false

redis:
  address: localhost:6379
  # Note: Password is set via LIVEKIT_REDIS_PASSWORD env var

logging:
  level: info

The node_ip setting is critical for NAT traversal. Without it, LiveKit may fail to start with "panic: invalid argument to Intn". Always set this to your VM's public IP address.

Port Requirements

Open these ports in your firewall:

PortProtocolServiceRequired For
7881TCPLiveKit RTCWebRTC over TCP
7882UDPLiveKit TURNWebRTC NAT traversal
5060UDP/TCPSIP SignalingPhone calls
10000-20000UDPRTP MediaVoice audio

GCP Firewall Example:

# LiveKit RTC ports
gcloud compute firewall-rules create allow-livekit \
  --allow tcp:7881,udp:7882 \
  --target-tags=livekit-server

# SIP and RTP ports
gcloud compute firewall-rules create allow-sip \
  --allow udp:5060,tcp:5060,udp:10000-20000 \
  --target-tags=livekit-server

# Tag your VM
gcloud compute instances add-tags <vm-name> --tags=livekit-server

Twilio SIP Trunk Setup

  1. Create SIP Trunk in Twilio Console:

    • Go to Elastic SIP Trunking > Trunks
    • Create trunk with domain: your-app.pstn.twilio.com
  2. Configure Termination (for outbound calls):

    • Create credential list in Voice > Credential Lists
    • Add credentials to trunk under Termination > Authentication
  3. Configure Origination (for inbound calls):

    twilio api trunking v1 trunks origination-urls create \
      --trunk-sid <trunk_sid> \
      --sip-url "sip:<your-vm-ip>:5060" \
      --weight 1 --priority 1 --enabled
  4. Create Outbound Trunk in LiveKit:

    Create outbound-trunk.json:

    {
      "trunk": {
        "name": "Twilio Outbound",
        "address": "your-app.pstn.twilio.com",
        "numbers": ["+91XXXXXXXXXX"],
        "auth_username": "<twilio-credential-username>",
        "auth_password": "<twilio-credential-password>"
      }
    }

    Create the trunk:

    lk sip outbound create outbound-trunk.json

    Save the returned trunk ID as LIVEKIT_SIP_TRUNK_ID.


Docker Compose for Production

Complete production docker-compose.prod.yml:

version: "3.9"

services:
  api:
    image: agent-studio:latest
    restart: unless-stopped
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
      - JWT_SECRET=${JWT_SECRET}
      - ENCRYPTION_KEY=${ENCRYPTION_KEY}
      - LIVEKIT_URL=ws://localhost:7880
      - LIVEKIT_API_KEY=${LIVEKIT_API_KEY}
      - LIVEKIT_API_SECRET=${LIVEKIT_API_SECRET}
      - LOG_LEVEL=INFO
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health/ready"]
      interval: 30s
      timeout: 10s
      retries: 3
    deploy:
      replicas: 2
      resources:
        limits:
          cpus: '1'
          memory: 1G

  worker:
    image: agent-studio:latest
    restart: unless-stopped
    command: ["python", "-m", "agent_studio.worker.entrypoint", "start"]
    # Allow workers to reach LiveKit on host network
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
      # Use host.docker.internal since LiveKit runs with network_mode: host
      - LIVEKIT_URL=ws://host.docker.internal:7880
      - LIVEKIT_API_KEY=${LIVEKIT_API_KEY}
      - LIVEKIT_API_SECRET=${LIVEKIT_API_SECRET}
      - LIVEKIT_SIP_TRUNK_ID=${LIVEKIT_SIP_TRUNK_ID}
      - API_BASE_URL=http://api:8000
    deploy:
      replicas: 2
      resources:
        limits:
          cpus: '2'
          memory: 2G

  livekit:
    image: livekit/livekit-server:latest
    network_mode: host
    volumes:
      - ./livekit.yaml:/etc/livekit.yaml:ro
    command: --config /etc/livekit.yaml
    depends_on:
      - redis
    restart: unless-stopped

  sip:
    image: livekit/sip:latest
    network_mode: host
    environment:
      - SIP_API_KEY=${LIVEKIT_API_KEY}
      - SIP_API_SECRET=${LIVEKIT_API_SECRET}
      - SIP_WS_URL=ws://localhost:7880
      - SIP_PORT=5060
      - SIP_RTP_PORT=10000-20000
      - SIP_REDIS_HOST=localhost:6379
      - SIP_USE_EXTERNAL_IP=true
    depends_on:
      - livekit
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: agent_studio
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

Health Checks

The Docker image includes health checks:

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD python -c "import httpx; httpx.get('http://localhost:8000/health/live', timeout=5)"

Health endpoints:

  • /health/live - Container is running
  • /health/ready - Ready to accept traffic
  • /health - Full health with dependencies

Resource Requirements

Minimum (Development)

  • CPU: 2 cores
  • RAM: 4GB
  • Disk: 10GB
  • CPU: 4+ cores
  • RAM: 8GB+
  • Disk: 50GB+ SSD

Per-Service Guidelines

ServiceCPURAMNotes
API0.5-1 core512MB-1GBScale horizontally
Worker1-2 cores1-2GBCPU for audio processing
LiveKit1-2 cores1-2GBMedia processing
SIP0.5-1 core512MBSIP signaling
PostgreSQL1-2 cores2-4GBSSD recommended
Redis0.5 core512MBFor session state

Troubleshooting

Container Won't Start

# Check logs
docker logs agent-studio-api

# Check if port is in use
lsof -i :8000

Database Connection Issues

# Test connectivity
docker run --rm -it postgres:16 \
  psql postgresql://user:pass@host:5432/dbname -c "SELECT 1"

LiveKit Connection Issues

# Check LiveKit is running
curl http://localhost:7880

# Check SIP server
docker logs sip

# Test SIP port
nc -zvu <your-ip> 5060

Workers can't connect to LiveKit (Connection refused to localhost:7880)

This happens because LiveKit runs with network_mode: host but workers run on Docker's bridge network. The solution is to use host.docker.internal:

worker:
  extra_hosts:
    - "host.docker.internal:host-gateway"
  environment:
    - LIVEKIT_URL=ws://host.docker.internal:7880

SIP Call Failures

# Check SIP server logs
docker logs sip --tail 100

# Verify trunk configuration
lk sip outbound list

# Check Twilio dashboard for call logs

Memory Issues

# Check container memory
docker stats agent-studio-api

# Increase limits
docker run --memory=2g ...

Permission Issues

The container runs as non-root user appuser (UID 1000). Ensure mounted volumes have correct permissions:

chown -R 1000:1000 /path/to/mounted/volume

On this page