# Deployment Migration Path: VPS → K3s → K8s

LLMHosts is designed to scale from a single VPS to a full Kubernetes cluster without rewriting configuration. Each tier builds on the previous one.

## Tier Overview

| Tier | When | Stack | Effort |
|------|------|-------|--------|
| **Docker Compose** | 1 server, < 10 users | `docker-compose.prod.yml` | 10 minutes |
| **K3s** | 1-3 servers, 10-100 users | Single-node K3s + manifests | 1 hour |
| **K8s** | 3+ servers, 100+ users | Managed K8s (EKS/GKE/AKS) | 1 day |

## Tier 1: Docker Compose (Current)

The default deployment. One server runs everything.

```
┌─────────────────────────────────┐
│  Server (VPS or bare metal)     │
│  ┌───────────┐ ┌─────────────┐  │
│  │  Traefik   │ │  LLMHosts   │  │
│  │  :80/:443  │ │  :4000      │  │
│  └───────────┘ └─────────────┘  │
│  ┌─────────────┐ ┌──────────┐  │
│  │  Frontend    │ │  Ollama  │  │
│  │  :3000       │ │  :11434  │  │
│  └─────────────┘ └──────────┘  │
└─────────────────────────────────┘
```

**Start here**: `scripts/bootstrap.sh` or `docker compose -f docker-compose.prod.yml up -d`

**Move to Tier 2 when**:
- You need more than one GPU host
- You want automated rollbacks on failed deployments
- You need pod-level health checks and auto-restart

## Tier 2: K3s (Single-Node Kubernetes)

K3s is a lightweight Kubernetes distribution that runs on a single server. It provides Kubernetes APIs with minimal overhead (~512MB RAM).

### Install K3s

```bash
# Install K3s with Traefik disabled (we use our own)
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--disable=traefik" sh -

# Verify
kubectl get nodes
```

### Convert Docker Compose to K3s

The Docker Compose services map directly to Kubernetes resources:

| Docker Compose | Kubernetes | Notes |
|----------------|------------|-------|
| `services.llmhost` | Deployment + Service | Replicas: 1 per GPU host |
| `services.frontend` | Deployment + Service | Stateless, can scale horizontally |
| `services.traefik` | DaemonSet + Ingress | Or use K3s's built-in Traefik |
| `volumes` | PersistentVolumeClaim | Local path or network storage |
| `.env` | Secret + ConfigMap | Sealed Secrets for production |

### Minimal K3s Manifests

```yaml
# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: llmhosts
---
# k8s/llmhost-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: llmhost
  namespace: llmhosts
spec:
  replicas: 1
  selector:
    matchLabels:
      app: llmhost
  template:
    metadata:
      labels:
        app: llmhost
    spec:
      containers:
        - name: llmhost
          image: ghcr.io/looknohandsmom/llmhosts.com:latest
          ports:
            - containerPort: 4000
          envFrom:
            - configMapRef:
                name: llmhost-config
            - secretRef:
                name: llmhost-secrets
          livenessProbe:
            httpGet:
              path: /health
              port: 4000
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /health
              port: 4000
            initialDelaySeconds: 5
            periodSeconds: 5
          resources:
            requests:
              memory: "512Mi"
              cpu: "250m"
            limits:
              memory: "4Gi"
              cpu: "2000m"
          volumeMounts:
            - name: data
              mountPath: /home/llmhost/.llmhosts
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: llmhost-data
---
apiVersion: v1
kind: Service
metadata:
  name: llmhost
  namespace: llmhosts
spec:
  selector:
    app: llmhost
  ports:
    - port: 4000
      targetPort: 4000
---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: llmhosts
  namespace: llmhosts
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
    - hosts:
        - llmhosts.com
      secretName: llmhosts-tls
  rules:
    - host: llmhosts.com
      http:
        paths:
          - path: /v1
            pathType: Prefix
            backend:
              service:
                name: llmhost
                port:
                  number: 4000
          - path: /health
            pathType: Exact
            backend:
              service:
                name: llmhost
                port:
                  number: 4000
          - path: /
            pathType: Prefix
            backend:
              service:
                name: frontend
                port:
                  number: 3000
```

### Migration Steps

1. **Install K3s** on the existing server (alongside Docker)
2. **Create namespace**: `kubectl apply -f k8s/namespace.yaml`
3. **Create secrets**: `kubectl create secret generic llmhost-secrets --from-env-file=.env -n llmhosts`
4. **Apply manifests**: `kubectl apply -f k8s/`
5. **Verify**: `kubectl get pods -n llmhosts`
6. **Switch DNS** to the K3s Ingress IP
7. **Stop Docker Compose**: `docker compose -f docker-compose.prod.yml down`

### K3s Advantages Over Docker Compose

- Automatic pod restart on crash
- Rolling deployments with zero downtime
- Built-in DNS resolution between services
- Resource limits enforced at the kernel level
- Standard Kubernetes API for tooling

## Tier 3: Managed Kubernetes (Multi-Node)

For production at scale, use a managed Kubernetes service.

### Provider Options

| Provider | Service | GPU Support | Starting Cost |
|----------|---------|-------------|---------------|
| AWS | EKS | A10G, A100, H100 | ~$75/mo + nodes |
| GCP | GKE | T4, A100, H100 | ~$75/mo + nodes |
| Azure | AKS | T4, A100, H100 | Free control plane + nodes |
| Civo | Managed K3s | None (CPU only) | ~$5/mo per node |

### What Changes from K3s

| Component | K3s | Managed K8s |
|-----------|-----|-------------|
| Storage | Local PV | EBS/PD/Azure Disk |
| Ingress | Traefik/nginx | ALB/GCE/AGIC |
| TLS | cert-manager | Cloud-managed or cert-manager |
| GPU scheduling | Manual | NVIDIA device plugin |
| Monitoring | Optional | Cloud-native (CloudWatch/Stackdriver) |
| Secrets | Sealed Secrets | Cloud KMS + External Secrets |

### GPU Node Pool

```yaml
# For EKS/GKE, add a GPU node pool
apiVersion: apps/v1
kind: Deployment
metadata:
  name: llmhost-gpu
  namespace: llmhosts
spec:
  replicas: 1
  template:
    spec:
      nodeSelector:
        cloud.google.com/gke-accelerator: nvidia-tesla-a100  # GKE
        # OR: nvidia.com/gpu.product: NVIDIA-A100-SXM4-80GB  # Generic
      containers:
        - name: llmhost
          image: ghcr.io/looknohandsmom/llmhosts.com:latest
          resources:
            limits:
              nvidia.com/gpu: 1
```

### Helm Chart (Future)

A Helm chart will be provided for simplified Kubernetes deployment:

```bash
helm repo add llmhosts https://charts.llmhosts.com
helm install llmhosts llmhosts/llmhosts \
  --set domain=llmhosts.com \
  --set gpu.enabled=true \
  --set replicas=2
```

## Migration Checklist

### Docker Compose → K3s

- [ ] Back up Docker volumes (`llmhosts_data`, `frontend_data`)
- [ ] Install K3s on the server
- [ ] Create Kubernetes secrets from `.env`
- [ ] Apply K3s manifests
- [ ] Verify pods are healthy
- [ ] Switch DNS to K3s Ingress
- [ ] Stop Docker Compose
- [ ] Verify TLS works
- [ ] Set up monitoring (Prometheus operator)

### K3s → Managed K8s

- [ ] Choose cloud provider and create cluster
- [ ] Set up cloud storage (PVC with cloud provider)
- [ ] Migrate secrets to cloud KMS
- [ ] Update Ingress for cloud load balancer
- [ ] Add GPU node pool (if needed)
- [ ] Deploy manifests to cloud cluster
- [ ] Verify health checks and scaling
- [ ] Switch DNS
- [ ] Decommission K3s server

## What Stays the Same

Across all tiers, these remain unchanged:
- **Docker image**: Same `Dockerfile`, same `llmhosts` binary
- **Environment variables**: Same `LLMHOSTS_*` config format
- **API endpoints**: `/v1/chat/completions`, `/v1/models`, `/health`
- **Health checks**: HTTP GET `/health` on port 4000
- **Data volume**: `/home/llmhost/.llmhosts` (keys, config, cache, audit DB)
