Running VMs on Kubernetes: A Guide to Installing KubeVirt in Your HomeLab

HomeLab tutorial - IT technology blog
HomeLab tutorial - IT technology blog

Why Run Virtual Machines on Kubernetes?

Most HomeLab enthusiasts start with Docker or a lightweight Kubernetes cluster to host media servers like Plex or simple web apps. Eventually, you hit a wall. You might need to run a specialized network appliance, a legacy Windows app that refuses to containerize, or a specific Linux kernel version. Usually, this forces you to maintain a separate hypervisor like Proxmox or ESXi.

Juggling two different control planes is a recipe for configuration drift and mental fatigue. I wanted a way to manage everything through a single API. KubeVirt bridges this gap by extending Kubernetes, allowing it to schedule and manage Virtual Machines just like any other Pod. I’ve run this setup for over a year to host legacy build servers, and the stability has been rock-solid while cutting my management overhead in half.

The Benefits of the KubeVirt Approach

  • Unified Networking: Your VM can sit behind a standard Kubernetes Service or Ingress. This means your Windows VM can benefit from the same Traefik or Nginx load balancer as your web containers.
  • Declarative Management: Define your VM in a YAML file. You can version control your entire infrastructure in Git and deploy it with a simple kubectl apply.
  • Resource Efficiency: You don’t need a dedicated hypervisor OS. If your nodes have spare RAM, they can run VMs. In my testing, the overhead of the KubeVirt launcher pod is negligible—typically under 100MB of RAM.

Installation: Preparing Your Cluster

Before jumping into the YAML, verify your hardware supports virtualization. KubeVirt runs KVM inside a pod. If your Kubernetes nodes are themselves virtual machines (nested virtualization), you must enable that feature in your underlying hypervisor first.

Step 1: Check Hardware Virtualization

Run this command on your nodes to see if the VMX (Intel) or SVM (AMD) extensions are active:

grep -E 'vmx|svm' /proc/cpuinfo

If the output is empty, check your BIOS settings. For those running on ARM boards like a Raspberry Pi, KubeVirt can use software emulation. Just be warned: it is painfully slow, often 10x slower than hardware-accelerated virtualization.

Step 2: Deploy the KubeVirt Operator

KubeVirt uses the Operator pattern to manage its own lifecycle. This handles the heavy lifting of installing the necessary Custom Resource Definitions (CRDs). Always pull the latest version string from the official releases to ensure compatibility.

# Grab the latest version
export VERSION=$(curl -s https://api.github.com/repos/kubevirt/kubevirt/releases/latest | grep tag_name | cut -d '"' -f 4)

# Deploy the Operator
kubectl create -f https://github.com/kubevirt/kubevirt/releases/download/${VERSION}/kubevirt-operator.yaml

# Deploy the KubeVirt Custom Resource
kubectl create -f https://github.com/kubevirt/kubevirt/releases/download/${VERSION}/kubevirt-cr.yaml

Step 3: Install virtctl

Standard kubectl commands work for basic tasks, but you will want the virtctl binary for more granular control. It handles VNC consoles, VM starts/stops, and image uploads. It’s a 20MB download that makes life much easier.

VERSION=$(curl -s https://api.github.com/repos/kubevirt/kubevirt/releases/latest | grep tag_name | cut -d '"' -f 4)
curl -L -o virtctl https://github.com/kubevirt/kubevirt/releases/download/${VERSION}/virtctl-${VERSION}-linux-amd64
chmod +x virtctl
sudo install virtctl /usr/local/bin/virtctl

Configuration: Storage and Images

Getting a multi-gigabyte ISO or QCOW2 image into a cluster is usually the hardest part of the process. KubeVirt uses the Containerized Data Importer (CDI) to automate this. It can pull images directly from an HTTP server or an S3 bucket and write them to a Persistent Volume.

Installing CDI

export VERSION=$(curl -s https://api.github.com/repos/kubevirt/containerized-data-importer/releases/latest | grep tag_name | cut -d '"' -f 4)
kubectl create -f https://github.com/kubevirt/containerized-data-importer/releases/download/${VERSION}/cdi-operator.yaml
kubectl create -f https://github.com/kubevirt/containerized-data-importer/releases/download/${VERSION}/cdi-cr.yaml

Creating Your First Virtual Machine

With CDI running, we can define a VM. This example uses a Fedora Cloud image. The DataVolume section tells CDI to fetch the 400MB image and prepare a 10GB disk for us automatically.

apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: fedora-vm
spec:
  running: false
  template:
    spec:
      domain:
        devices:
          disks:
          - disk: {bus: virtio}
            name: datavolumedisk
        resources:
          requests:
            memory: 2Gi
      volumes:
      - dataVolume:
          name: fedora-dv
        name: datavolumedisk
  dataVolumeTemplates:
  - metadata:
      name: fedora-dv
    spec:
      storage:
        resources:
          requests:
            storage: 10Gi
        storageClassName: local-path
      source:
        http:
          url: "https://download.fedoraproject.org/pub/fedora/linux/releases/38/Cloud/x86_64/images/Fedora-Cloud-Base-38-1.6.x86_64.qcow2"

Apply the manifest. You will see an ‘importer’ pod spin up. On a gigabit connection, it usually takes about 45 seconds to download and verify the Fedora image before the VM is ready to boot.

Verification & Monitoring

I usually set running: false in the initial YAML. This prevents the VM from trying to boot while the image is still downloading. Once the PVC status shows as ‘Bound’, you are ready to go.

Starting the VM

Fire up the machine using your new CLI tool:

virtctl start fedora-vm

Accessing the Console

KubeVirt’s integration with the terminal is excellent. You can jump into the serial console without needing to find an IP address or configure SSH first:

virtctl console fedora-vm

For Windows VMs or Linux desktops, use the VNC tunnel. This command opens a local port and launches your default VNC viewer: virtctl vnc fedora-vm.

Key Takeaways from the Trenches

  • Prioritize VirtIO: Always use bus: virtio. In my benchmarks, VirtIO drivers provide up to 3x faster disk I/O compared to emulated SATA.
  • Automate with Cloud-Init: Don’t waste time manually creating users. Use cloudInitNoCloud to inject your SSH public keys and network configs during the first boot.
  • Advanced Networking: If you need your VM to have a real IP on your home VLAN (rather than a 10.x.x.x cluster IP), look into Multus CNI. It allows you to bridge the VM directly to your physical network interface.

Setting up KubeVirt turns your Kubernetes cluster into a true private cloud. While YAML-based VM management has a slight learning curve, the ability to manage your entire stack in one place is a massive upgrade for any HomeLab.

Share: