Tech Blog by vClusterPress and Media Resources

Day 5: CI/CD with vind: The setup-vind GitHub Action

Mar 12, 2026
|
5
min Read
Day 5: CI/CD with vind: The setup-vind GitHub Action

We’ve been running vind locally all week. But one of the most common uses for local Kubernetes is CI/CD, spinning up ephemeral clusters for end-to-end testing.

If you’re currently using setup-kind in your GitHub Actions, you can switch to setup-vind with minimal changes.

Today, let’s build a real CI/CD pipeline using the setup-vind GitHub Action.

Why vind in CI/CD?

With KinD in CI, you get a basic cluster. With vind, you get:

  • No image loading:  No need for kind load docker-image. The registry proxy shares the host’s containerd storage, so images built in your workflow are available inside the cluster immediately.
  • Automatic log export: Container logs are saved as GitHub Actions artifacts automatically, even on failure.
  • Multi-cluster setups:  Easily create multiple clusters in the same workflow.

CI/CD Workflow

Basic Setup

Here’s the simplest possible workflow:

# .github/workflows/e2e.yaml

name: E2E Tests with vind

on:
 push:
   branches: [main]
 pull_request:
   branches: [main]

permissions:
 contents: read

jobs:
 e2e:
   runs-on: ubuntu-latest
   steps:
     - uses: actions/checkout@v4

     - name: Create vind cluster
       uses: loft-sh/setup-vind@v1
       with:
         name: e2e-cluster

     - name: Verify cluster
       run: |
         kubectl get nodes
         kubectl get namespaces

That’s it. The setup-vind action:

  1. Installs the latest vCluster CLI (or the version you specify)
  2. Runs vcluster use driver docker
  3. Creates the cluster with vcluster create
  4. Waits for it to be ready
  5. Sets up kubeconfig so kubectl works immediately

Real-World E2E Pipeline

Let’s build a complete pipeline that deploys an app and runs tests. I set up a demo repo at saiyam1814/vind-demo with this workflow and it passed on the first run.

The Application

A simple Kubernetes deployment with 3 replicas:

# k8s/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
 name: vind-demo
spec:
 replicas: 3
 selector:
   matchLabels:
     app: vind-demo
 template:
   metadata:
     labels:
       app: vind-demo
   spec:
     containers:
     - name: vind-demo
       image: nginx:alpine
       ports:
       - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
 name: vind-demo
spec:
 type: ClusterIP
 ports:
 - port: 80
   targetPort: 80
 selector:
   app: vind-demo

The Workflow

# .github/workflows/e2e.yaml

name: E2E Tests with vind

on:
 push:
   branches: [main]
 pull_request:
   branches: [main]

permissions:
 contents: read

jobs:
 e2e:
   runs-on: ubuntu-latest
   steps:
     - uses: actions/checkout@v4

     - name: Create vind cluster
       uses: loft-sh/setup-vind@v1
       with:
         name: e2e-cluster

     - name: Verify cluster
       run: |
         echo "=== Nodes ==="
         kubectl get nodes -o wide
         echo "=== Namespaces ==="
         kubectl get namespaces
         echo "=== System Pods ==="
         kubectl get pods -A

     - name: Deploy application
       run: |
         kubectl apply -f k8s/deployment.yaml
         kubectl rollout status deployment/vind-demo --timeout=180s

     - name: Verify deployment
       run: |
         echo "=== Pods ==="
         kubectl get pods -o wide
         echo "=== Services ==="
         kubectl get svc
         echo "=== Deployment ==="
         kubectl get deployment vind-demo

         READY=$(kubectl get deployment vind-demo -o jsonpath='{.status.readyReplicas}')
         if [ "$READY" != "3" ]; then
           echo "Expected 3 ready replicas, got $READY"
           exit 1
         fi
         echo "All 3 replicas are ready!"

     - name: Test service connectivity
       run: |
         SVC_IP=$(kubectl get svc vind-demo -o jsonpath='{.spec.clusterIP}')
         echo "Service ClusterIP: $SVC_IP"
         kubectl run curl-test --image=curlimages/curl:latest \
           --restart=Never --rm -i --timeout=60s \
           -- curl -sf http://$SVC_IP

Real CI Output

Here’s what actually happens when this workflow runs on GitHub Actions:

=== Nodes ===
NAME          STATUS   ROLES                  AGE   VERSION    INTERNAL-IP   OS-IMAGE             CONTAINER-RUNTIME
e2e-cluster   Ready    control-plane,master   20s   v1.35.0    172.18.0.2    Ubuntu 24.04.4 LTS   containerd://2.1.6

=== Pods ===
NAME                         READY   STATUS    RESTARTS   AGE
vind-demo-xxxxxxxxx-xxxxx    1/1     Running   0          4s
vind-demo-xxxxxxxxx-xxxxx    1/1     Running   0          4s
vind-demo-xxxxxxxxx-xxxxx    1/1     Running   0          4s

=== Services ===
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP   25s
vind-demo    ClusterIP   10.108.7.150    <none>        80/TCP    5s

All 3 replicas are ready!
Service ClusterIP: 10.108.7.150

The entire workflow  cluster creation, deployment, verification, and service connectivity test  completes in about 90 seconds. You can see the actual passing run in the repo.

What Happens on Cleanup

After the job completes (pass or fail), setup-vind automatically:

  1. Exports container logs as GitHub Actions artifacts (unless skipClusterLogsExport: true)
  2. Deletes the cluster (unless skipClusterDeletion: true)

This means you get debugging logs even on failure,  no manual cleanup needed.

Configuration Options

Parameter Default Description
version latest vCluster CLI version
name vind Cluster name
config (empty) Path to vcluster.yaml values file
kubernetes-version (empty) Kubernetes version to use
skipClusterDeletion false Keep cluster after job ends
skipClusterLogsExport false Skip log artifact export

Using a Config File

For multi-node CI clusters or custom settings:

#vcluster.yaml

experimental:
 docker:
   nodes:
     - name: worker-1
     - name: worker-2
- uses: loft-sh/setup-vind@v1
 with:
   config:vcluster.yaml

Multi-Cluster Workflows

Need to test multi-cluster scenarios? Create multiple clusters in the same job:

- uses: loft-sh/setup-vind@v1
 with:
   name: platform
- uses: loft-sh/setup-vind@v1
 with:
   name: agent

Each call to `setup-vind` creates an independent cluster. The last cluster created will be the active kubectl context.

Migrating from setup-kind

If you’re currently using setup-kind, here’s the mapping:

setup-kind setup-vind Notes
version: v0.30.0 version: v0.32.1 vCluster CLI version, not KinD
image: kindest/node:v1.35.0 kubernetes-version: "1.35.0" No node images needed
config: kind.yaml config: vcluster.yaml Different config format
kind load docker-image (not needed) Shared containerd storage makes images available

Before (setup-kind):

- uses: engineerd/setup-kind@v0.5.0
 with:
   version: v0.20.0
   image: kindest/node:v1.35.0

- name: Load images
 run: kind load docker-image my-app:latest

After (setup-vind):

- uses: loft-sh/setup-vind@v1
 with:
   version: v0.32.1
   kubernetes-version: "1.35.0"

# No image loading needed - Docker images are accessible directly

Tomorrow: Advanced Features

We’ve covered the basics,  local clusters, multi-node, external nodes, and CI/CD. Tomorrow, let’s dive into the features that make vind really shine for day-to-day development: sleep/wake for resource management, the registry proxy deep dive, and custom CNI/CSI configurations.

vind is open source: github.com/loft-sh/vind , so do star the repo if you like vind

Share:
Ready to take vCluster for a spin?

Deploy your first virtual cluster today.