Skip to main content

Overview

This guide covers two primary deployment paths for Tesslate Studio:
  1. Docker Compose for single-server deployments (development teams, small organizations).
  2. Kubernetes for production-scale deployments with auto-scaling, per-project namespace isolation, and EBS VolumeSnapshot persistence.
Each path includes step-by-step instructions, from provisioning infrastructure to verifying a healthy deployment.
This guide assumes you have completed the Self-Hosting Quickstart and are familiar with the basics. If not, start there first.

Choosing a Deployment Path

FactorDocker ComposeKubernetes (Minikube)Kubernetes (AWS EKS)
Best forSingle server, small teamsLocal K8s testingProduction, multi-user
ScalingVertical only (bigger server)Not designed for scaleHorizontal (node autoscaling)
Project isolationDocker networksNamespace per projectNamespace + NetworkPolicy
StorageLocal filesystemPVC (hostpath)EBS VolumeSnapshots
SSL/TLSTraefik + Let’s EncryptHTTP onlycert-manager + Cloudflare
ComplexityLowMediumHigh
Monthly cost20to20 to 120 (server)Free (local)~280to280 to 320 (AWS baseline)

Path 1: Docker Compose (Single Server)

This path deploys all services on a single Linux server with Docker Compose, Traefik for reverse proxying, and optional Let’s Encrypt for SSL.

Prerequisites

Server

  • Cloud VM or dedicated server
  • 16 GB RAM recommended
  • 50 GB+ disk space
  • Ubuntu 22.04 LTS (or similar)

Domain

  • Custom domain (e.g., studio.yourcompany.com)
  • DNS access to create A records
  • Wildcard DNS support (*.studio.yourcompany.com)

Step-by-Step Setup

1

Provision and prepare the server

# Connect to your server
ssh root@your-server-ip

# Update system packages
apt update && apt upgrade -y

# Install Docker
curl -fsSL https://get.docker.com | sh
systemctl enable docker
systemctl start docker

# Create a non-root user
adduser tesslate
usermod -aG docker tesslate
usermod -aG sudo tesslate

# Set up firewall
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

# Switch to the new user
su - tesslate
2

Configure DNS

Add these DNS records at your provider:
TypeNameValueTTL
Astudioyour-server-ip300
A*.studioyour-server-ip300
Wait 5 to 10 minutes, then verify:
dig studio.yourcompany.com
3

Clone and configure

cd /home/tesslate
git clone https://github.com/TesslateAI/Studio.git
cd Studio
cp .env.example .env
Edit .env with production values:
# Domain
APP_DOMAIN=studio.yourcompany.com
COOKIE_DOMAIN=.studio.yourcompany.com
COOKIE_SECURE=true
CORS_ORIGINS=https://studio.yourcompany.com

# Security (generate strong keys)
SECRET_KEY=<generate-with-python>

# AI
LITELLM_API_BASE=https://your-litellm-proxy.com/v1
LITELLM_MASTER_KEY=sk-<your-key>

# Database (change default password)
POSTGRES_PASSWORD=STRONG_PASSWORD_HERE
DATABASE_URL=postgresql+asyncpg://tesslate:STRONG_PASSWORD_HERE@postgres:5432/tesslate

# Logging
LOG_LEVEL=INFO
4

Configure SSL with Let's Encrypt (Traefik)

Update your docker-compose.yml Traefik service for production SSL:
services:
  traefik:
    image: traefik:v3.1
    command:
      - "--api.dashboard=false"
      - "--providers.docker=true"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
      - "--certificatesresolvers.letsencrypt.acme.email=admin@yourcompany.com"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
Create the certificate storage directory:
mkdir -p letsencrypt
5

Build the devserver image and start services

# Build the devserver image (required for user project containers)
docker build -t tesslate-devserver:latest -f orchestrator/Dockerfile.devserver orchestrator/

# Start all services
docker compose up --build -d

# Monitor startup
docker compose logs -f
6

Verify the deployment

# Check all services are running
docker compose ps

# Test the health endpoint
curl https://studio.yourcompany.com/api/health
# Expected: {"status": "healthy"}
Open https://studio.yourcompany.com in your browser. You should see a valid SSL certificate and the Tesslate Studio login page.

Database Backups

Create an automated backup script at /home/tesslate/backup.sh:
#!/bin/bash
BACKUP_DIR="/home/tesslate/backups"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/tesslate_$DATE.sql.gz"

mkdir -p $BACKUP_DIR
docker compose exec -T postgres pg_dump -U tesslate tesslate_db | gzip > $BACKUP_FILE
find $BACKUP_DIR -type f -mtime +7 -delete

echo "Backup completed: $BACKUP_FILE"
Schedule it with cron (daily at 2 AM):
chmod +x /home/tesslate/backup.sh
crontab -e
# Add: 0 2 * * * /home/tesslate/backup.sh

Performance Tuning

Add tuning parameters to your docker-compose.yml postgres service:
services:
  postgres:
    command:
      - "postgres"
      - "-c"
      - "shared_buffers=256MB"
      - "-c"
      - "effective_cache_size=1GB"
      - "-c"
      - "maintenance_work_mem=128MB"
      - "-c"
      - "checkpoint_completion_target=0.9"
      - "-c"
      - "wal_buffers=16MB"
services:
  orchestrator:
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 4G
        reservations:
          cpus: '1.0'
          memory: 2G

  app:
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 2G
services:
  orchestrator:
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Path 2: Kubernetes (Production)

Kubernetes mode provides per-project namespace isolation, EBS VolumeSnapshot-based persistence, NetworkPolicy security, automatic SSL, and horizontal scaling. This section covers both Minikube (local testing) and AWS EKS (production).

Kubernetes Architecture

+-----------------------------------------------------------+
|  Kubernetes Cluster                                        |
+-----------------------------------------------------------+
|  Namespace: tesslate                                       |
|  +-------------+  +--------------+  +--------------+      |
|  |  Frontend   |  | Orchestrator |  |  PostgreSQL  |      |
|  |    Pod      |  |     Pod      |  |     Pod      |      |
|  +-------------+  +--------------+  +--------------+      |
|                                                            |
|  Namespace: proj-{uuid-1}                                  |
|  +--------------------------------------------------+     |
|  |  File Manager Pod + Dev Container Pods            |     |
|  |  PVC (10Gi EBS)   |   NetworkPolicy (isolated)    |     |
|  +--------------------------------------------------+     |
|                                                            |
|  Namespace: proj-{uuid-2}                                  |
|  +--------------------------------------------------+     |
|  |  File Manager Pod + Dev Container Pods            |     |
|  +--------------------------------------------------+     |
|                                                            |
|  Namespace: ingress-nginx                                  |
|  +--------------------------------------------------+     |
|  |  NGINX Ingress Controller (SSL termination)       |     |
|  +--------------------------------------------------+     |
+-----------------------------------------------------------+
Each user project gets its own Kubernetes namespace (proj-{uuid}) with:
  • A dedicated PVC for block storage
  • A file manager pod (always running, handles file operations)
  • Dev container pods (frontend, backend, database as needed)
  • An Ingress resource for subdomain routing
  • A NetworkPolicy enforcing zero cross-project communication

Option A: Minikube (Local Kubernetes Testing)

Use Minikube to test the full Kubernetes deployment locally before going to production.
1

Install prerequisites

SoftwareInstall Command
Docker Desktopdocker.com/products/docker-desktop
Minikubebrew install minikube (macOS) or choco install minikube (Windows)
kubectlbrew install kubernetes-cli (macOS) or choco install kubernetes-cli (Windows)
2

Create the Minikube cluster

minikube start -p tesslate --driver=docker --memory=4096 --cpus=2
minikube -p tesslate addons enable ingress
3

Build and load all images

Minikube runs its own Docker daemon. Build images on your host, then load them into Minikube:
# Build all three images
docker build --no-cache -t tesslate-backend:latest -f orchestrator/Dockerfile orchestrator/
docker build --no-cache -t tesslate-frontend:latest -f app/Dockerfile.prod app/
docker build --no-cache -t tesslate-devserver:latest -f orchestrator/Dockerfile.devserver orchestrator/

# Load into Minikube
minikube -p tesslate image load tesslate-backend:latest
minikube -p tesslate image load tesslate-frontend:latest
minikube -p tesslate image load tesslate-devserver:latest
Minikube caches images by tag. If you rebuild an image with the same tag, you must delete the old image from Minikube first:
minikube -p tesslate ssh -- docker rmi -f tesslate-backend:latest
Then rebuild and reload.
4

Configure secrets

cp k8s/.env.example k8s/.env.minikube
# Edit k8s/.env.minikube with your SECRET_KEY, LITELLM keys, etc.
Required values:
  • SECRET_KEY (random string for JWT)
  • DATABASE_URL (default works for in-cluster PostgreSQL)
  • LITELLM_API_BASE and LITELLM_MASTER_KEY
5

Deploy

kubectl apply -k k8s/overlays/minikube

# Wait for pods to start
kubectl rollout status deployment/postgres -n tesslate --timeout=120s
kubectl rollout status deployment/tesslate-backend -n tesslate --timeout=120s
kubectl rollout status deployment/tesslate-frontend -n tesslate --timeout=120s
6

Access the application

Option 1: Tunnel (in a separate terminal)
minikube -p tesslate tunnel
Then open http://localhost in your browser.Option 2: Port-forward
kubectl port-forward -n tesslate svc/tesslate-frontend-service 5000:80
kubectl port-forward -n tesslate svc/tesslate-backend-service 8000:8000
Then open http://localhost:5000.
FeatureMinikubeAWS EKS
VolumeSnapshots / TimelineNot supportedEBS snapshots
Hibernation with snapshotsProjects stop; no snapshotSnapshot created, namespace deleted
SSL/TLSHTTP onlyAutomatic via cert-manager
Data persistencePVC survives pod restarts; lost if cluster deletedSnapshot-based, survives cluster changes

Option B: AWS EKS (Production)

For production deployments, use AWS EKS with Terraform for Infrastructure as Code.

Infrastructure Provisioning with Terraform

Terraform provisions all required AWS resources: VPC, EKS cluster, ECR repositories, S3 bucket, IAM roles, and Helm charts.
1

Install prerequisites

# Terraform
brew install terraform    # macOS
choco install terraform   # Windows

# AWS CLI
brew install awscli       # macOS
choco install awscli      # Windows

# kubectl
brew install kubernetes-cli
2

Configure AWS credentials

aws configure
# AWS Access Key ID: [your-access-key]
# AWS Secret Access Key: [your-secret-key]
# Default region: us-east-1
# Default output format: json
3

Configure Terraform variables

cd k8s/terraform/aws
cp terraform.tfvars.example terraform.tfvars
Edit terraform.tfvars:
project_name            = "tesslate"
environment             = "production"
domain_name             = "studio.yourcompany.com"

# EKS
eks_cluster_version     = "1.28"
eks_node_instance_types = ["t3.large"]
eks_node_min_size       = 1
eks_node_max_size       = 10
eks_node_desired_size   = 2

# VPC
vpc_cidr               = "10.0.0.0/16"
availability_zones     = ["us-east-1a", "us-east-1b"]

# S3
s3_bucket_prefix       = "tesslate-projects"

tags = {
  Project     = "Tesslate Studio"
  Environment = "Production"
  ManagedBy   = "Terraform"
}
4

Provision infrastructure

terraform init
terraform plan
terraform apply
This takes approximately 15 to 20 minutes. Terraform creates:
ResourceDetails
VPCPublic and private subnets across 2 AZs, NAT gateway
EKS ClusterManaged control plane with IRSA enabled
Node GroupsPrimary (on-demand t3.large) and optional Spot group
EKS Add-onsCoreDNS, kube-proxy, VPC CNI, EBS CSI Driver
ECR Repositoriestesslate-backend, tesslate-frontend, tesslate-devserver
S3 BucketVersioned, encrypted, lifecycle-managed project storage
IAM RolesIRSA roles for S3 access and EBS provisioning
Storage Classtesslate-block-storage (EBS gp3, encrypted)
5

Configure kubectl

aws eks update-kubeconfig --region us-east-1 --name tesslate-cluster
kubectl get nodes
6

Build and push images to ECR

# Login to ECR
aws ecr get-login-password --region us-east-1 | \
  docker login --username AWS --password-stdin \
  <AWS_ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com

# Build and push backend
docker build --no-cache -t tesslate-backend:latest -f orchestrator/Dockerfile orchestrator/
docker tag tesslate-backend:latest <AWS_ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/tesslate-backend:latest
docker push <AWS_ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/tesslate-backend:latest

# Build and push frontend
docker build --no-cache -t tesslate-frontend:latest -f app/Dockerfile.prod app/
docker tag tesslate-frontend:latest <AWS_ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/tesslate-frontend:latest
docker push <AWS_ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/tesslate-frontend:latest

# Build and push devserver
docker build --no-cache -t tesslate-devserver:latest -f orchestrator/Dockerfile.devserver orchestrator/
docker tag tesslate-devserver:latest <AWS_ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/tesslate-devserver:latest
docker push <AWS_ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/tesslate-devserver:latest
Always use --no-cache when building images for deployment. This ensures all code changes are included in the image.
7

Create Kubernetes secrets

kubectl create secret generic tesslate-app-secrets -n tesslate \
  --from-literal=SECRET_KEY=your-production-secret \
  --from-literal=DATABASE_URL=postgresql+asyncpg://tesslate:STRONG_PASS@postgres:5432/tesslate \
  --from-literal=LITELLM_API_BASE=https://your-litellm.com/v1 \
  --from-literal=LITELLM_MASTER_KEY=sk-your-key
8

Deploy Kubernetes manifests

kubectl apply -k k8s/overlays/aws

# Wait for pods
kubectl rollout status deployment/tesslate-backend -n tesslate --timeout=300s
kubectl rollout status deployment/tesslate-frontend -n tesslate --timeout=300s
9

Verify

# Check pods
kubectl get pods -n tesslate -o wide

# Check ingress
kubectl get ingress -n tesslate

# Check certificates
kubectl get certificate -n tesslate

EBS VolumeSnapshot Persistence

In production Kubernetes mode, user projects are persisted using EBS VolumeSnapshots (not S3). Here is how the lifecycle works:
1

Project opens (restore)

The orchestrator creates a namespace (proj-{uuid}) and a PVC. If a VolumeSnapshot exists from a previous session, the PVC is created with a dataSource pointing to the snapshot. EBS lazy-loads data on first access, so startup is near-instant (under 10 seconds).
2

Runtime (fast local I/O)

All file edits happen on the PVC, which provides EBS block storage speeds. All containers in the project share the same PVC via pod affinity (scheduled on the same node).Users can manually create snapshots via the Timeline UI (up to 5 per project).
3

Hibernation (snapshot and cleanup)

When a project is idle for longer than K8S_HIBERNATION_IDLE_MINUTES (default: 10), a cleanup CronJob triggers:
  1. Creates a VolumeSnapshot from the PVC (takes under 5 seconds to initiate)
  2. Waits for the snapshot to be ready
  3. Deletes the namespace (cascading to all resources)
The snapshot remains in EBS for fast restoration.
4

Deletion (soft delete with 30-day retention)

When a user deletes a project, snapshots are soft-deleted with a 30-day expiry. A daily cleanup CronJob (3 AM UTC) permanently deletes expired snapshots.

AWS Cost Estimation

ResourceMonthly Cost
EKS Cluster (control plane)~$73
EC2 Nodes (2 x t3.large on-demand)~$121
EBS Volumes (~100 GB total)~$10
S3 Storage (~10 GB)~$0.23
NAT Gateway (2 AZs)~$66
Data Transfer~10to10 to 50
Total baseline~280to280 to 320
  • Spot instances for user containers: Add a Spot node group (90% savings on compute for dev containers).
  • Cluster autoscaling: Scale down during off-hours.
  • S3 lifecycle policies: Move old versions to Infrequent Access (30 days) then Glacier (90 days).
  • EBS gp3: Already used by default; 20% cheaper than gp2.
  • Right-size nodes: Use t3.medium if resource usage is low.

Kustomize Manifest Structure

Tesslate Studio uses Kustomize for Kubernetes manifest management:
k8s/
  base/                        # Environment-agnostic manifests
    kustomization.yaml
    namespace/                 # tesslate namespace
    core/                      # Backend, frontend, cleanup CronJobs
    database/                  # PostgreSQL deployment
    ingress/                   # Main platform ingress
    security/                  # RBAC, NetworkPolicies, resource quotas
    storage/                   # VolumeSnapshotClass
    minio/                     # Local S3 (Minikube only)

  overlays/
    minikube/                  # Local dev: local images, no TLS, MinIO
      kustomization.yaml
      backend-patch.yaml
      secrets/

    aws/                       # Production: ECR images, TLS, native S3
      kustomization.yaml
      backend-patch.yaml
      secrets/
Deploy commands:
# Preview generated manifests (dry run)
kubectl kustomize k8s/overlays/minikube

# Apply Minikube overlay
kubectl apply -k k8s/overlays/minikube

# Apply AWS overlay
kubectl apply -k k8s/overlays/aws

# Check diff before applying
kubectl diff -k k8s/overlays/aws

Updating a Running Deployment

# 1. Delete old image from Minikube (critical; it caches by tag)
minikube -p tesslate ssh -- docker rmi -f tesslate-backend:latest

# 2. Rebuild
docker build --no-cache -t tesslate-backend:latest -f orchestrator/Dockerfile orchestrator/

# 3. Load new image
minikube -p tesslate image load tesslate-backend:latest

# 4. Restart pod
kubectl delete pod -n tesslate -l app=tesslate-backend

# 5. Wait for ready
kubectl rollout status deployment/tesslate-backend -n tesslate --timeout=120s

Security Hardening

1

Disable root SSH login

Edit /etc/ssh/sshd_config:
PermitRootLogin no
PasswordAuthentication no
Then restart SSH: sudo systemctl restart ssh
2

Install Fail2Ban

sudo apt install fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
3

Enable automatic security updates

sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades
4

Change default database password

Update POSTGRES_PASSWORD and DATABASE_URL in your .env or Kubernetes secrets.

Troubleshooting

Check Traefik logs:
docker compose logs traefik | grep -i acme
Common causes:
  • DNS records not propagated yet (wait 10 to 15 minutes)
  • Port 80 blocked by firewall
  • Rate limit hit (Let’s Encrypt allows 5 certificates per week per domain)
Force renewal:
rm -rf letsencrypt/acme.json
docker compose restart traefik
kubectl get certificate -n tesslate
kubectl describe certificate tesslate-wildcard-tls -n tesslate
kubectl logs -n cert-manager deployment/cert-manager --tail=50
Ensure the Cloudflare API token has Zone:Zone:Read and Zone:DNS:Edit permissions.
Cause: Minikube caches images by tag and does not overwrite.Fix: Always delete the old image before loading the new one:
minikube -p tesslate ssh -- docker rmi -f tesslate-backend:latest
docker build --no-cache -t tesslate-backend:latest -f orchestrator/Dockerfile orchestrator/
minikube -p tesslate image load tesslate-backend:latest
kubectl delete pod -n tesslate -l app=tesslate-backend
kubectl describe pod -n proj-{uuid} | grep Image
# Verify it shows the correct ECR URL

kubectl exec -n tesslate deployment/tesslate-backend -- env | grep K8S_DEVSERVER
# Should show the full ECR URL for tesslate-devserver
# Check if the project pod is running
kubectl get pods -n proj-{uuid}

# Check dev server logs
kubectl logs -n proj-{uuid} {pod-name} -c dev-server

# Check ingress
kubectl get ingress -n proj-{uuid}

# Restart NGINX Ingress if endpoint cache is stale
kubectl rollout restart deployment/ingress-nginx-controller -n ingress-nginx
# Check for finalizers
kubectl get ns proj-{uuid} -o yaml

# Remove stuck finalizers
kubectl patch ns proj-{uuid} -p '{"metadata":{"finalizers":[]}}' --type=merge
Cause: NGINX Ingress Controller has a stale backend endpoint cache.Fix:
kubectl rollout restart deployment/ingress-nginx-controller -n ingress-nginx
kubectl rollout status deployment/ingress-nginx-controller -n ingress-nginx --timeout=120s

Monitoring and Maintenance

Health Checks

EndpointPurpose
GET /healthBackend liveness check (returns 200 if alive)
GET /api/configPublic configuration (deployment mode, app domain)
Kubernetes probes (configured in backend-deployment.yaml):
  • Startup probe: /health, 10s initial delay, allows 2 minutes for boot
  • Liveness probe: /health every 10 seconds
  • Readiness probe: /health every 5 seconds

Useful Commands

# Docker Compose: view resource usage
docker stats --no-stream

# Kubernetes: view pod resource usage
kubectl top pods -n tesslate
kubectl top nodes

# Kubernetes: view logs
kubectl logs -n tesslate deployment/tesslate-backend --tail=100
kubectl logs -n tesslate deployment/tesslate-backend -f

# Kubernetes: list user project namespaces
kubectl get ns | grep proj-

# Kubernetes: clean up orphaned namespaces
kubectl delete ns proj-{uuid}

Next Steps

Configuration Reference

All environment variables with detailed explanations

Architecture Overview

Understand the system design, data flow, and security model

Quickstart

Get started with Docker Compose locally

API Documentation

Explore the REST API