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
- Clone the repository:
git clone https://github.com/tap-health/agent-studio.git
cd agent-studio- Copy environment file:
cp .env.example .env- Start all services:
docker-compose -f docker/docker-compose.yml up -d- Check service status:
docker-compose -f docker/docker-compose.yml psServices
The development stack includes:
| Service | Port | Description |
|---|---|---|
api | 8000 | FastAPI server with hot reload |
worker | - | LiveKit voice worker |
db | 5432 | PostgreSQL 16 |
redis | 6379 | Redis 7 |
livekit | 7880 | LiveKit 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
- API: http://localhost:8000
- API Docs: http://localhost:8000/docs
- PostgreSQL:
postgresql://postgres:postgres@localhost:5432/agent_studio - Redis:
redis://localhost:6379
Development Workflow
The API service mounts your source code and enables hot reload:
volumes:
- ../src:/app/src:roChanges 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 apiStopping 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 -vProduction 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-idSelf-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
│
▼
PSTNDocker 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-stoppedSIP 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: infoThe ${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: infoThe 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:
| Port | Protocol | Service | Required For |
|---|---|---|---|
| 7881 | TCP | LiveKit RTC | WebRTC over TCP |
| 7882 | UDP | LiveKit TURN | WebRTC NAT traversal |
| 5060 | UDP/TCP | SIP Signaling | Phone calls |
| 10000-20000 | UDP | RTP Media | Voice 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-serverTwilio SIP Trunk Setup
-
Create SIP Trunk in Twilio Console:
- Go to Elastic SIP Trunking > Trunks
- Create trunk with domain:
your-app.pstn.twilio.com
-
Configure Termination (for outbound calls):
- Create credential list in Voice > Credential Lists
- Add credentials to trunk under Termination > Authentication
-
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 -
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.jsonSave 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
Recommended (Production with SIP)
- CPU: 4+ cores
- RAM: 8GB+
- Disk: 50GB+ SSD
Per-Service Guidelines
| Service | CPU | RAM | Notes |
|---|---|---|---|
| API | 0.5-1 core | 512MB-1GB | Scale horizontally |
| Worker | 1-2 cores | 1-2GB | CPU for audio processing |
| LiveKit | 1-2 cores | 1-2GB | Media processing |
| SIP | 0.5-1 core | 512MB | SIP signaling |
| PostgreSQL | 1-2 cores | 2-4GB | SSD recommended |
| Redis | 0.5 core | 512MB | For session state |
Troubleshooting
Container Won't Start
# Check logs
docker logs agent-studio-api
# Check if port is in use
lsof -i :8000Database 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> 5060Workers 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:7880SIP Call Failures
# Check SIP server logs
docker logs sip --tail 100
# Verify trunk configuration
lk sip outbound list
# Check Twilio dashboard for call logsMemory 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