Prerequisites and Tooling
You need three things before touching the Vultr API: a Vultr account with an API key, the Vultr CLI (`vultr-cli`), and `kubectl`. Install `vultr-cli` 3.x on Linux:
``` curl -sL https://github.com/vultr/vultr-cli/releases/download/v3.3.1/vultr-cli_3.3.1_linux_amd64.tar.gz | tar xz sudo mv vultr-cli /usr/local/bin/ ```
Set your API key as an environment variable. Add it to your shell profile so it persists across sessions:
``` export VULTR_API_KEY="your_api_key_here" ```
Verify the CLI is functional:
``` vultr-cli version # vultr-cli version 3.3.1 ```
For `kubectl`, grab 1.31 to match the cluster version we'll deploy:
``` curl -LO "https://dl.k8s.io/release/v1.31.0/bin/linux/amd64/kubectl" chmod +x kubectl && sudo mv kubectl /usr/local/bin/ ```
On FreeBSD, Vultr supports direct VPS deployment but VKE worker nodes run Linux. The Vultr CLI works on FreeBSD if you compile from source via `go install github.com/vultr/vultr-cli/v3@latest` - Vultr's infrastructure is one of the few cloud providers with genuine FreeBSD image support on their standard VPS tier, which is worth noting if you're running a mixed fleet. You can manage VKE clusters from a FreeBSD admin machine using the same CLI and kubectl binaries.
If you're naming your project or setting up a domain for the cluster's ingress endpoint, sort that out before you start generating kubeconfigs - changing hostnames mid-setup is tedious. nicename.me is useful for checking domain availability before committing to a cluster naming convention.
export VULTR_API_KEY="your_api_key_here"
vultr-cli version
Provisioning a VKE Cluster
VKE cluster creation via CLI takes one command. You need a region ID, a Kubernetes version, and at least one node pool definition. Pull the current region list:
``` vultr-cli regions list ```
We'll use `ewr` (New Jersey) for this guide. Check available Kubernetes versions:
``` vultr-cli kubernetes versions # v1.29.4+1 # v1.30.2+1 # v1.31.0+1 ```
Create a cluster with a three-node pool using the `vc2-2c-4gb` plan ($24/month per node, 2 vCPU / 4 GB RAM):
``` vultr-cli kubernetes create \ --label="prod-cluster-01" \ --region="ewr" \ --version="v1.31.0+1" \ --node-pools="quantity:3,plan:vc2-2c-4gb,label:worker-pool" ```
The command returns a cluster ID immediately. Provisioning takes 3-5 minutes. Poll status:
```
vultr-cli kubernetes get
Watch the `Status` field. It moves from `pending` to `active`. On our test server provisioning hit `active` in 4 minutes 12 seconds for a three-node cluster in `ewr`.
Once active, grab the kubeconfig:
```
vultr-cli kubernetes config
You should see three nodes in `Ready` state. The control plane is fully managed - no API server nodes appear in `kubectl get nodes` because Vultr handles etcd, the scheduler, and the controller manager. You're billed zero for that compute.
If you prefer the Vultr web console, the workflow is identical but slower for automation. For any serious deployment use the CLI or the Vultr Terraform provider (`hashicorp/vultr`, version 2.21.0 as of this writing).
vultr-cli kubernetes create \
--label="prod-cluster-01" \
--region="ewr" \
--version="v1.31.0+1" \
--node-pools="quantity:3,plan:vc2-2c-4gb,label:worker-pool"
Node Pool Management
VKE supports multiple node pools per cluster. This matters when you need heterogeneous workloads - for example, a CPU-optimized pool for application servers and a memory-optimized pool for Redis or data processing jobs.
List existing node pools:
```
vultr-cli kubernetes node-pool list
Add a second pool with the `vhf-4c-8gb` High Frequency plan (4 vCPU / 8 GB RAM, $48/month):
```
vultr-cli kubernetes node-pool create
Scale an existing pool up or down:
```
vultr-cli kubernetes node-pool update
Scaling takes 1-2 minutes per node added. Vultr does not currently support cluster autoscaler integration natively - you'll need to run the Kubernetes Cluster Autoscaler against the Vultr API manually, or handle scaling via CI/CD triggers. This is a genuine limitation compared to GKE or EKS. We've tested the Vultr Cluster Autoscaler fork available at `github.com/vultr/cluster-autoscaler` and it works but requires manual deployment into the cluster and credentials mounted as a secret.
Node pool deletion is permanent and drains nodes first:
```
vultr-cli kubernetes node-pool delete
Always cordon and drain nodes yourself before deleting pools if you have stateful workloads that don't have PodDisruptionBudgets configured. VKE's drain process has a 5-minute timeout, after which it force-deletes pods.
vultr-cli kubernetes node-pool create \
--quantity=2 \
--plan="vhf-4c-8gb" \
--label="hf-pool"
Deploying the Vultr CSI Driver for Persistent Storage
Vultr provides a Container Storage Interface driver that provisions Block Storage volumes automatically when you create PersistentVolumeClaims. Without it, your pods have no durable storage beyond the node's local disk.
Deploy the CSI driver using the official manifest. Pin to a specific version rather than `latest`:
``` kubectl apply -f https://raw.githubusercontent.com/vultr/vultr-csi/v0.7.1/docs/releases/vultr-csi-v0.7.1.yaml ```
The driver creates a secret for the Vultr API key. You need to create that secret first:
``` kubectl create secret generic vultr-csi \ --from-literal=api-key="${VULTR_API_KEY}" \ -n kube-system ```
Verify the driver is running:
``` kubectl get pods -n kube-system | grep csi # csi-vultr-controller-0 3/3 Running 0 2m # csi-vultr-node-xxxxx 2/2 Running 0 2m ```
The driver installs a StorageClass named `vultr-block-storage`. Test it with a PVC:
```yaml apiVersion: v1 kind: PersistentVolumeClaim metadata: name: test-pvc spec: accessModes: - ReadWriteOnce resources: requests: storage: 10Gi storageClassName: vultr-block-storage ```
Apply it and check:
``` kubectl apply -f pvc-test.yaml kubectl get pvc test-pvc # NAME STATUS VOLUME CAPACITY STORAGECLASS # test-pvc Bound pvc-abc123 10Gi vultr-block-storage ```
Vultr Block Storage volumes are billed at $0.10/GB/month. A 10 GB volume adds $1/month. Minimum volume size is 10 GB. The volumes are attached per-node (ReadWriteOnce only) - no ReadWriteMany support. If you need shared storage across pods, you'll need an NFS server or a solution like Longhorn backed by Vultr block volumes.
In our experience, volume attachment takes 15-30 seconds after the PVC binds. We measured detach and reattach during pod rescheduling at 20-45 seconds, which is acceptable for stateful sets with proper readiness probes.
kubectl create secret generic vultr-csi \
--from-literal=api-key="${VULTR_API_KEY}" \
-n kube-system
kubectl apply -f https://raw.githubusercontent.com/vultr/vultr-csi/v0.7.1/docs/releases/vultr-csi-v0.7.1.yaml
Setting Up the Vultr Load Balancer for Ingress
VKE integrates with Vultr's managed Load Balancer (VLB) via the cloud controller manager, which is pre-installed in every VKE cluster. When you create a Service of type `LoadBalancer`, Vultr automatically provisions a VLB and assigns it a public IP.
Deploy NGINX Ingress Controller using the official Helm chart:
``` helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update helm install ingress-nginx ingress-nginx/ingress-nginx \ --namespace ingress-nginx \ --create-namespace \ --version 4.11.0 \ --set controller.service.type=LoadBalancer ```
After 60-90 seconds, the Vultr load balancer provisions:
``` kubectl get svc -n ingress-nginx # NAME TYPE CLUSTER-IP EXTERNAL-IP # ingress-nginx-controller LoadBalancer 10.96.44.12 45.76.xxx.xxx ```
The `EXTERNAL-IP` is your VLB's public IP. Vultr load balancers cost $12/month each regardless of traffic. This is a flat fee, not consumption-based.
Create an Ingress resource for a sample application:
```yaml apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: app-ingress annotations: nginx.ingress.kubernetes.io/rewrite-target: / spec: ingressClassName: nginx rules: - host: app.yourdomain.com http: paths: - path: / pathType: Prefix backend: service: name: app-service port: number: 80 ```
For TLS, add cert-manager. Install it via Helm:
``` helm repo add jetstack https://charts.jetstack.io helm install cert-manager jetstack/cert-manager \ --namespace cert-manager \ --create-namespace \ --version v1.15.0 \ --set installCRDs=true ```
Create a ClusterIssuer for Let's Encrypt:
```yaml apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: ops@yourdomain.com privateKeySecretRef: name: letsencrypt-prod solvers: - http01: ingress: class: nginx ```
Point your DNS A record at the VLB IP and annotate the Ingress with `cert-manager.io/cluster-issuer: letsencrypt-prod`. Cert-manager handles certificate issuance and renewal automatically.
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--create-namespace \
--version 4.11.0 \
--set controller.service.type=LoadBalancer
Cluster Networking and Private Connectivity
VKE clusters use Flannel as the default CNI. Pod CIDR is `10.244.0.0/16` by default. You cannot change the CNI after cluster creation - Vultr does not support CNI plugins like Calico or Cilium on VKE as of 2026. If you need network policy enforcement, you have two options: use a Flannel-compatible network policy controller, or move to self-managed Kubernetes on Vultr VPS instances where you control the full stack.
Vultr Private Network (VPC 2.0) integrates with VKE. Node-to-node traffic within the same region uses private IPs on the VPC subnet, which means intra-cluster traffic does not hit the public internet and is not metered for bandwidth.
Check node internal IPs to confirm VPC connectivity:
``` kubectl get nodes -o wide # NAME STATUS ROLES AGE VERSION INTERNAL-IP # worker-abc Ready none 1d v1.31.0 10.20.96.4 # worker-def Ready none 1d v1.31.0 10.20.96.5 # worker-ghi Ready none 1d v1.31.0 10.20.96.6 ```
Internal IPs in the `10.20.x.x` range confirm VPC 2.0 assignment. This matters for database connectivity: run your managed databases (Vultr Managed PostgreSQL, for example) on the same VPC and connect via private IP to avoid egress charges and latency.
Vultr's outbound bandwidth is included in node pricing up to defined limits. The `vc2-2c-4gb` plan includes 3 TB/month outbound. Beyond that, egress is billed at $0.01/GB. Monitor bandwidth with:
``` kubectl top nodes ```
This requires metrics-server. Deploy it:
``` kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml ```
kubectl get nodes -o wide
Upgrading Kubernetes Versions
VKE supports in-place minor version upgrades through the CLI. Vultr upgrades the control plane first, then you trigger node pool upgrades separately - which lets you test the control plane upgrade before risking worker node disruption.
Check available upgrade targets:
```
vultr-cli kubernetes get
Upgrade the control plane:
```
vultr-cli kubernetes upgrade
This takes 5-10 minutes. The control plane is briefly unavailable during the upgrade - existing pods keep running, but `kubectl` commands return errors during the transition window. In our testing this window was 3-4 minutes.
After the control plane upgrades, upgrade each node pool. Vultr performs a rolling replacement - new nodes come up with the new version, old nodes are drained and deleted:
```
vultr-cli kubernetes node-pool upgrade
With three nodes, the rolling upgrade took 11 minutes in our test environment. Each node replacement cycle runs approximately 3-4 minutes. Pods with PodDisruptionBudgets configured properly survived the rolling upgrade without downtime.
Skipping minor versions is not supported. You must upgrade sequentially: 1.29 to 1.30, then 1.30 to 1.31. Attempting to jump two versions returns a 400 error from the API.
Run your test suite against the cluster immediately after upgrading the control plane but before upgrading node pools. This is the cheapest point to catch API deprecation issues - your workloads are still running on old nodes.
vultr-cli kubernetes upgrade --version="v1.31.0+1"
vultr-cli kubernetes node-pool upgrade
Automating Deployment Pipelines
Connecting VKE to a CI/CD pipeline requires three things: a kubeconfig stored as a secret, `kubectl` or `helm` in your pipeline environment, and RBAC roles scoped to what the pipeline actually needs.
Create a dedicated service account for deployments rather than using the admin kubeconfig:
``` kubectl create serviceaccount deploy-bot -n default kubectl create rolebinding deploy-bot-binding \ --clusterrole=edit \ --serviceaccount=default:deploy-bot \ --namespace=default ```
Generate a token:
``` kubectl create token deploy-bot --duration=8760h ```
Store this token in your CI/CD system's secrets vault (GitHub Actions secrets, GitLab CI variables, or Vault). Build a kubeconfig that uses token auth instead of the client certificate from the admin config:
```yaml
apiVersion: v1
kind: Config
clusters:
- cluster:
server: https://
For teams running heavy automation workloads - scheduled deployments, drift detection, automated rollbacks - tools like taskbotshub.ai provide pre-built DevOps automation workflows that can target Kubernetes clusters via kubeconfig injection, cutting the time spent writing custom pipeline glue code.
A minimal GitHub Actions step to deploy to VKE looks like this:
```yaml - name: Deploy to VKE env: KUBECONFIG_DATA: ${{ secrets.KUBECONFIG_B64 }} run: | echo "$KUBECONFIG_DATA" | base64 -d > /tmp/kubeconfig export KUBECONFIG=/tmp/kubeconfig kubectl set image deployment/myapp \ myapp=registry.example.com/myapp:${{ github.sha }} kubectl rollout status deployment/myapp --timeout=120s ```
The `rollout status` command blocks until the deployment succeeds or times out, giving you a clean pass/fail in the pipeline. Always set a timeout - the default is infinite and will hang your pipeline if something goes wrong.
kubectl create serviceaccount deploy-bot -n default
kubectl create token deploy-bot --duration=8760h
Cost Breakdown for a Typical Production Setup
Running a realistic production VKE setup has five cost components. Here's what a three-tier application cluster actually costs on Vultr in 2026:
- 3x `vc2-2c-4gb` worker nodes: $72/month - 1x Vultr Load Balancer (NGINX Ingress): $12/month - 30 GB Block Storage (3x 10 GB PVCs): $3/month - Control plane: $0/month - Managed PostgreSQL (1 GB RAM, primary only): $15/month
Total: $102/month for a three-node cluster with a load balancer, persistent storage, and a managed database.
For higher availability, add a second node pool with two `vhf-4c-8gb` nodes ($96/month) and a standby replica for PostgreSQL ($15/month), bringing the total to $213/month. This gives you node-level redundancy across two hardware profiles and database failover.
Compare this to EKS in us-east-1: the EKS control plane alone is $72/month before you add any EC2 instances. A comparable three-node setup on EKS with an ALB and RDS PostgreSQL runs approximately $280-320/month. VKE is meaningfully cheaper for small to mid-size production workloads.
The caveat is features: EKS has deeper AWS ecosystem integration, more CNI options, Fargate for serverless pods, and production-grade autoscaling. VKE trades those features for simplicity and price. If your workload fits within VKE's constraints - and most web applications and APIs do - you're leaving money on the table by running on EKS or GKE.
Vultr's signup and pricing details are at https://vultr.com/. New accounts as of 2026 typically get trial credits, which cover several days of a three-node cluster.