--- title: Kubernetes Deployment subtitle: Deploy Skyvern at scale with Kubernetes manifests slug: self-hosted/kubernetes --- This guide walks through deploying Skyvern on Kubernetes for production environments. Use this when you need horizontal scaling, high availability, or integration with existing Kubernetes infrastructure. Do not expose this deployment to the public internet without adding authentication at the ingress layer. ## Prerequisites - A running Kubernetes cluster (1.19+) - `kubectl` configured to access your cluster - An ingress controller (the manifests use Traefik, but any controller works) - An LLM API key (OpenAI, Anthropic, Azure, etc.) ## Architecture overview The Kubernetes deployment creates three services: ```mermaid flowchart LR subgraph Kubernetes Cluster Ingress[Ingress] --> FE[Frontend
:8080] Ingress --> BE[Backend
:8000] Ingress --> Art[Artifacts
:9090] BE --> DB[(PostgreSQL
:5432)] BE --> Browser[Browser
embedded] FE --> Art end BE --> LLM[LLM Provider] ``` | Component | Service | Purpose | |-----------|---------|---------| | Backend | `skyvern-backend` | API server + embedded browser | | Frontend | `skyvern-frontend` | Web UI + artifact server | | PostgreSQL | `postgres` | Database for tasks, workflows, credentials | --- ## Quick start ### 1. Clone the repository ```bash git clone https://github.com/Skyvern-AI/skyvern.git cd skyvern/kubernetes-deployment ``` ### 2. Configure backend secrets Edit `backend/backend-secrets.yaml` with your LLM provider credentials: ```yaml backend/backend-secrets.yaml apiVersion: v1 kind: Secret metadata: name: skyvern-backend-env namespace: skyvern type: Opaque stringData: ENV: local # LLM Configuration - set your provider ENABLE_OPENAI: "true" OPENAI_API_KEY: "sk-your-api-key-here" LLM_KEY: "OPENAI_GPT4O" # Database - points to the PostgreSQL service DATABASE_STRING: "postgresql+psycopg://skyvern:skyvern@postgres/skyvern" # Browser settings BROWSER_TYPE: "chromium-headless" BROWSER_ACTION_TIMEOUT_MS: "5000" MAX_STEPS_PER_RUN: "50" # Server PORT: "8000" LOG_LEVEL: "INFO" ``` For other LLM providers, see [LLM Configuration](/self-hosted/llm-configuration). ### 3. Configure frontend secrets Edit `frontend/frontend-secrets.yaml`: ```yaml frontend/frontend-secrets.yaml apiVersion: v1 kind: Secret metadata: name: skyvern-frontend-env namespace: skyvern type: Opaque stringData: VITE_API_BASE_URL: "http://skyvern.example.com/api/v1" VITE_WSS_BASE_URL: "ws://skyvern.example.com/api/v1" VITE_ARTIFACT_API_BASE_URL: "http://skyvern.example.com/artifacts" VITE_SKYVERN_API_KEY: "" # Leave empty for initial deploy ``` Replace `skyvern.example.com` with your actual domain. ### 4. Configure ingress Edit `ingress.yaml` with your domain and TLS settings: ```yaml ingress.yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: skyvern-ingress namespace: skyvern annotations: # Adjust for your ingress controller traefik.ingress.kubernetes.io/router.entrypoints: websecure spec: ingressClassName: traefik # Change to nginx, kong, etc. rules: - host: skyvern.example.com # Your domain http: paths: - path: /api pathType: Prefix backend: service: name: skyvern-backend port: number: 8000 - path: /artifacts pathType: Prefix backend: service: name: skyvern-frontend port: number: 9090 - path: / pathType: Prefix backend: service: name: skyvern-frontend port: number: 8080 ``` ### 5. Deploy Run the deployment script: ```bash chmod +x k8s-deploy.sh ./k8s-deploy.sh ``` This applies manifests in order: 1. Namespace 2. PostgreSQL (secrets, storage, deployment, service) 3. Backend (secrets, deployment, service) 4. Frontend (secrets, deployment, service) 5. Ingress ### 6. Verify deployment Check that all pods are running: ```bash kubectl get pods -n skyvern ``` Expected output: ``` NAME READY STATUS RESTARTS AGE postgres-xxx 1/1 Running 0 2m skyvern-backend-xxx 1/1 Running 0 1m skyvern-frontend-xxx 1/1 Running 0 30s ``` The backend pod takes 1-2 minutes to become ready as it runs database migrations. ### 7. Get your API key Wait for the backend pod to show `1/1` in the `READY` column of `kubectl get pods -n skyvern` before running this command. The API key file is generated during startup and won't exist until the pod is ready. ```bash kubectl exec -n skyvern deployment/skyvern-backend -- cat /app/.streamlit/secrets.toml ``` Copy the `cred` value and update `frontend/frontend-secrets.yaml`: ```yaml VITE_SKYVERN_API_KEY: "eyJhbGciOiJIUzI1..." ``` Reapply the frontend secrets and restart: ```bash kubectl apply -f frontend/frontend-secrets.yaml -n skyvern kubectl rollout restart deployment/skyvern-frontend -n skyvern ``` ### 8. Access the UI Navigate to your configured domain (e.g., `https://skyvern.example.com`). You should see the Skyvern dashboard. --- ## Manifest structure ``` kubernetes-deployment/ ├── namespace.yaml # Creates 'skyvern' namespace ├── k8s-deploy.sh # Deployment script ├── ingress.yaml # Ingress configuration ├── backend/ │ ├── backend-secrets.yaml # Environment variables │ ├── backend-deployment.yaml # Pod spec │ └── backend-service.yaml # ClusterIP service ├── frontend/ │ ├── frontend-secrets.yaml # Environment variables │ ├── frontend-deployment.yaml # Pod spec │ └── frontend-service.yaml # ClusterIP service └── postgres/ ├── postgres-secrets.yaml # Database credentials ├── postgres-storage.yaml # PersistentVolumeClaim ├── postgres-deployment.yaml # Pod spec └── postgres-service.yaml # ClusterIP service ``` --- ## Storage configuration By default, the manifests use `hostPath` volumes. This works for single-node clusters but isn't suitable for multi-node production deployments. ### Using PersistentVolumeClaims For production, replace `hostPath` with PVCs. Edit `backend/backend-deployment.yaml`: ```yaml volumes: - name: artifacts persistentVolumeClaim: claimName: skyvern-artifacts-pvc - name: videos persistentVolumeClaim: claimName: skyvern-videos-pvc ``` Create the PVCs: ```yaml skyvern-storage.yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: skyvern-artifacts-pvc namespace: skyvern spec: accessModes: - ReadWriteOnce resources: requests: storage: 50Gi --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: skyvern-videos-pvc namespace: skyvern spec: accessModes: - ReadWriteOnce resources: requests: storage: 100Gi ``` ### Using S3 or Azure Blob For cloud storage, configure the backend environment variables instead of mounting volumes. See [Storage Configuration](/self-hosted/storage). --- ## Scaling ### Horizontal scaling To run multiple backend instances, increase the replica count: ```yaml backend/backend-deployment.yaml spec: replicas: 3 # Run 3 backend pods ``` Each pod runs its own browser instance. Tasks are distributed across pods. When scaling horizontally, ensure your storage backend supports concurrent access (S3, Azure Blob, or ReadWriteMany PVCs). Local storage with ReadWriteOnce PVCs won't work across multiple pods. ### Resource limits Add resource limits to prevent pods from consuming excessive resources: ```yaml containers: - name: skyvern-backend resources: requests: memory: "2Gi" cpu: "500m" limits: memory: "4Gi" cpu: "2000m" ``` Browser instances need significant memory. Start with 2GB minimum per pod. --- ## TLS configuration To enable HTTPS, uncomment the TLS section in `ingress.yaml`: ```yaml spec: tls: - hosts: - skyvern.example.com secretName: skyvern-tls-secret ``` Create the TLS secret: ```bash kubectl create secret tls skyvern-tls-secret \ --cert=path/to/tls.crt \ --key=path/to/tls.key \ -n skyvern ``` Or use cert-manager for automatic certificate management. Update frontend secrets to use `https` and `wss`: ```yaml VITE_API_BASE_URL: "https://skyvern.example.com/api/v1" VITE_WSS_BASE_URL: "wss://skyvern.example.com/api/v1" ``` --- ## Using an external database For production, consider using a managed PostgreSQL service (RDS, Cloud SQL, Azure Database). 1. Remove the `postgres/` manifests from the deployment 2. Update `backend/backend-secrets.yaml`: ```yaml DATABASE_STRING: "postgresql+psycopg://user:password@your-db-host:5432/skyvern" ``` --- ## Troubleshooting ### Pods stuck in Pending Check for resource constraints: ```bash kubectl describe pod -n skyvern ``` Common causes: - Insufficient node resources - PersistentVolume not available - Image pull errors ### Backend crashes on startup Check the logs: ```bash kubectl logs -n skyvern deployment/skyvern-backend ``` Common causes: - Invalid LLM API key - Database connection failed - Missing environment variables ### Frontend shows "Unauthorized" The API key in frontend secrets doesn't match the generated key. Re-copy it from the backend pod. ### Ingress not routing correctly Verify your ingress controller is running and the ingress resource is configured: ```bash kubectl get ingress -n skyvern kubectl describe ingress skyvern-ingress -n skyvern ``` --- ## Cleanup To remove the entire deployment: ```bash kubectl delete namespace skyvern ``` This removes all resources in the `skyvern` namespace. To clean up host storage (if using hostPath): ```bash rm -rf /data/artifacts /data/videos /data/har /data/log /app/.streamlit ``` --- ## Next steps Configure S3 or Azure Blob for artifact storage Configure additional LLM providers