Skip to main content
The Kubiya Control Plane can be self-hosted on your own infrastructure, giving you full control over your agent orchestration platform with VPC-only access, compliance boundaries, and custom integrations.
Run Anywhere: Deploy on local machines, cloud VMs (AWS, GCP, Azure), Kubernetes clusters, or bare metal servers with full access to private resources.

Overview

The Control Plane is a FastAPI-based REST API server that orchestrates your entire Kubiya infrastructure. Key Responsibilities:
  • Manages agent lifecycle and configuration
  • Integrates with Temporal for workflow orchestration
  • Handles teams, projects, environments, and policies
  • Streams real-time execution events via WebSocket
  • Routes tasks to appropriate queues and workers

Quick Start

1

Create Database

createdb kubiya_control_plane
2

Configure Connection

export DATABASE_URL='postgresql://user:pass@localhost:5432/kubiya_control_plane'
URL-encode special characters: !%21, @%40, #%23
3

Start Server

kubiya control-plane start
Automatically installs the latest version from PyPI.
4

Verify

curl http://localhost:7777/api/health
Visit http://localhost:7777/api/docs for interactive API documentation.

Prerequisites

System Requirements

  • Python 3.10+ (NOT 3.8+)
  • PostgreSQL 13+ (required)
  • Redis (optional, recommended)
  • 2GB RAM minimum (4GB recommended)

Database Setup

# macOS
brew install postgresql@15
brew services start postgresql@15

# Ubuntu/Debian
sudo apt-get install postgresql-15
sudo systemctl start postgresql

# Create database
createdb kubiya_control_plane

Optional: Redis

# macOS
brew install redis && brew services start redis

# Ubuntu/Debian
sudo apt-get install redis-server && sudo systemctl start redis

# Docker
docker run -d -p 6379:6379 redis:7-alpine

Configuration

Package Installation

By default, the CLI installs the latest version from PyPI automatically.
# Latest version
kubiya control-plane start

# Specific version
kubiya control-plane start --package-version=0.6.0

Server Options

# Default configuration
kubiya control-plane start

# Custom host/port
kubiya control-plane start --host=127.0.0.1 --port=8080

# Production with 4 workers
kubiya control-plane start --workers=4

# Development with hot reload
kubiya control-plane start --development

# With LiteLLM gateway
kubiya control-plane start --litellm-gateway-url=http://localhost:4000
Development Mode (--development):
  • Hot reloading on code changes
  • Single worker process
  • NOT for production
Production Mode (--workers=N):
  • Gunicorn with multiple workers
  • Better performance and fault tolerance
  • Recommended: 4 workers
Route LLM requests through your own proxy:
kubiya control-plane start \
  --litellm-gateway-url=http://localhost:4000 \
  --litellm-api-key=your-key
Benefits: centralized management, cost tracking, rate limiting

Environment Variables

VariableRequiredDefaultDescription
DATABASE_URL-PostgreSQL connection string
API_HOST0.0.0.0Server bind address
API_PORT7777Server port
SECRET_KEY⚠️AutoJWT secret (set for production!)
REDIS_URLredis://localhost:6379/0Redis connection
TEMPORAL_HOSTlocalhost:7233Temporal server
LITELLM_API_KEY-LiteLLM API key
LITELLM_GATEWAY_URL-LiteLLM gateway URL
SUPABASE_URL-Supabase project URL
SUPABASE_SERVICE_KEY-Supabase service key
Use CLI flags --workers and --development for worker count and hot reloading.
Production Security: Never use auto-generated SECRET_KEY in production!
export SECRET_KEY="$(openssl rand -base64 32)"

Production Deployment

Docker

FROM python:3.11-slim
RUN curl -fsSL https://cli.kubiya.ai/install.sh | sh
WORKDIR /app

ENV API_PORT=7777
HEALTHCHECK CMD curl -f http://localhost:7777/api/health || exit 1

CMD ["kubiya", "control-plane", "start"]

Kubernetes

apiVersion: apps/v1
kind: Deployment
metadata:
  name: control-plane
spec:
  replicas: 3
  selector:
    matchLabels:
      app: control-plane
  template:
    metadata:
      labels:
        app: control-plane
    spec:
      containers:
      - name: control-plane
        image: your-registry/control-plane:latest
        ports:
        - containerPort: 7777
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: control-plane-secrets
              key: database-url
        - name: SECRET_KEY
          valueFrom:
            secretKeyRef:
              name: control-plane-secrets
              key: secret-key
        resources:
          requests:
            memory: "512Mi"
            cpu: "250m"
          limits:
            memory: "2Gi"
            cpu: "1000m"
        livenessProbe:
          httpGet:
            path: /api/health
            port: 7777
          initialDelaySeconds: 60
        readinessProbe:
          httpGet:
            path: /api/health
            port: 7777
          initialDelaySeconds: 30

Nginx Reverse Proxy

upstream control_plane {
    server localhost:7777;
}

server {
    listen 443 ssl http2;
    server_name api.your-domain.com;

    ssl_certificate /etc/ssl/certs/your-domain.crt;
    ssl_certificate_key /etc/ssl/private/your-domain.key;
    ssl_protocols TLSv1.2 TLSv1.3;

    location / {
        proxy_pass http://control_plane;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Database Management

Automatic Migrations

Migrations run automatically on startup using Alembic:
kubiya control-plane start
# [3/4] Running database migrations...
# ✓ Database migrations complete

Backup & Restore

# Backup
pg_dump $DATABASE_URL > backup_$(date +%Y%m%d_%H%M%S).sql

# Restore
psql $DATABASE_URL < backup_20260105_140000.sql

# Automated backups (cron)
0 2 * * * pg_dump $DATABASE_URL | gzip > /backups/cp_$(date +\%Y\%m\%d).sql.gz

Connection Pooling

Use PgBouncer for production:
[databases]
kubiya = host=localhost port=5432 dbname=kubiya

[pgbouncer]
listen_port = 6432
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 25

Monitoring

Health Checks

curl http://localhost:7777/api/health
# {"status":"ok"}

API Documentation

Once running, access:
  • Swagger UI: http://localhost:7777/api/docs
  • ReDoc: http://localhost:7777/api/redoc
  • OpenAPI Spec: http://localhost:7777/api/openapi.json

Built-in WebUI

# Default (auto-assigned port)
kubiya control-plane start

# Custom port
kubiya control-plane start --webui-port=9999

# Disable
kubiya control-plane start --no-webui
The WebUI provides real-time logs, health status, and process info.

Logs

tail -f ~/.kubiya/control-plane/logs/server.log
cat ~/.kubiya/control-plane/logs/server.log | jq '.message'

Security

Production Checklist

Best Practices

Never use auto-generated keys in production:
# Generate strong key
export SECRET_KEY="$(openssl rand -base64 32)"

# Use secret managers
export SECRET_KEY="$(aws secretsmanager get-secret-value \
  --secret-id kubiya/control-plane/secret-key \
  --query SecretString --output text)"
Never pass secrets via CLI flags (visible in process list)
Always use SSL/TLS:
export DATABASE_URL='postgresql://user:pass@host:5432/db?sslmode=require'
Grant minimal permissions:
CREATE USER kubiya_cp WITH PASSWORD 'secure_password';
GRANT CONNECT ON DATABASE kubiya TO kubiya_cp;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO kubiya_cp;
Bind to localhost for local-only access:
kubiya control-plane start --host=127.0.0.1
Always use reverse proxy for external access with:
  • SSL/TLS termination
  • Rate limiting
  • WAF protection

Troubleshooting

Error: migration failed: connection refusedSolutions:
  1. Verify PostgreSQL is running: pg_isready -h localhost -p 5432
  2. Check DATABASE_URL format: echo $DATABASE_URL
  3. Test connection: psql $DATABASE_URL -c "SELECT 1"
  4. Check firewall: nc -zv postgres-host 5432
Error: server did not become healthySolutions:
  1. Find process: lsof -i:7777
  2. Kill process: lsof -ti:7777 | xargs kill
  3. Use different port: --port=8888
Error: could not translate host nameCause: Special characters in password not encodedSolution: Encode special characters:
  • !%21, @%40, #%23, $%24, %%25
# Wrong
DATABASE_URL='postgresql://user:pass!word@host:5432/db'

# Correct
DATABASE_URL='postgresql://user:pass%21word@host:5432/db'
Or use Python: python3 -c "from urllib.parse import quote; print(quote('pass!word'))"
Error: ModuleNotFoundErrorSolutions:
  1. Reinstall: kubiya control-plane start --no-cache
  2. Specific version: kubiya control-plane start --package-version=0.6.0 --no-cache
  3. Use Git: --package-source=git+https://github.com/kubiyabot/control-plane-api.git@main
Warning: Using auto-generated SECRET_KEY (not suitable for production)Solution:
export SECRET_KEY="$(openssl rand -base64 32)"
kubiya control-plane start

Debug Mode

export LOG_LEVEL=DEBUG
kubiya control-plane start
tail -f ~/.kubiya/control-plane/logs/server.log | jq .

Next Steps