//------------------------------------------------------------------- //-------------------------------------------------------------------
KubeVirt K3s virtual machines Kubernetes

How to Run Virtual Machines on Kubernetes with KubeVirt and K3s

Running KubeVirt K3s virtual machines Kubernetes workloads with containers lets you keep a legacy Windows or Linux dependency alive without maintaining a separate hypervisor stack. This guide shows you how to install KubeVirt on a K3s cluster, launch a Linux VM from a cloud image, set up storage and networking, and understand live migration and backup limits.

Why Run Virtual Machines on Kubernetes with KubeVirt and K3s

Most teams don’t need KubeVirt K3s virtual machines Kubernetes to replace Proxmox or VMware; they use it for one legacy app, licensed tool, or Windows-only program that can’t be turned into a container, but it needs to sit next to all their other apps that already run on Kubernetes.

Without KubeVirt, you’d need two separate systems, one for containers, one for VMs, each with its own monitoring, access rules, and networking to manage. KubeVirt removes that split. The VM runs inside the same Kubernetes cluster, managed with the same kubectl commands, GitOps pipelines, and network policies as everything else.

This is the correct mental model for KubeVirt K3s virtual machines Kubernetes:

Think of KubeVirt as a bridge that lets VMs and containers run together on one platform, not as a replacement for full-scale virtualization.

If you need to run many VMs at datacenter scale, a dedicated Proxmox host is still the better choice.

Prerequisites for KubeVirt K3s Virtual Machines Kubernetes

Before deploying KubeVirt K3s virtual machines Kubernetes, confirm the following on every node that will schedule VM workloads:

  • A running K3s cluster, single-node or multi-node, on Ubuntu 22.04 or 24.04. If you haven’t set it up yet, check our K3s single-node install guide.
  • A physical server or a VM with nested virtualization turned on, where hardware virtualization such as Intel VT-x or AMD-V is enabled and visible to the operating system.
  • kubectl is configured with the cluster.
  • At least 4 vCPU and 8 GB RAM free on nodes that will run VMs.
  • A default StorageClass. K3s ships local-path-provisioner by default, which works for testing but isn’t ideal for live migration.

Step 1. Verify Hardware Virtualization

KubeVirt K3s virtual machines Kubernetes require KVM acceleration for acceptable performance. Check on each node:

apt install qemu-kvm libvirt-daemon-system -y
virt-host-validate qemu

You should see PASS next to both “Checking for hardware virtualization” and “Checking if device /dev/kvm exists“.

If you need an infrastructure to run KubeVirt K3s virtual machines Kubernetes workloads, choose a reliable dedicated server with confirmed VT-x or AMD-V support. Most standard cloud VPS plans don’t support nested virtualization properly, or disable it entirely. If that happens, KubeVirt quietly falls back to slow software emulation instead of telling you there’s a problem, so your VMs will run, just much slower than expected.

Step 2. Install K3s with Virtualization Support

If you haven’t installed K3s yet, install it using the stable channel:

curl -sfL https://get.k3s.io | INSTALL_K3S_CHANNEL=stable sh -s - server \
  --write-kubeconfig-mode 644

Confirm the node is ready:

sudo k3s kubectl get nodes
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
kubectl get nodes -o wide

Load the KVM kernel modules the KubeVirt K3s virtual machines Kubernetes stack depends on, and make them persistent across reboots:

sudo modprobe kvm vhost_net
echo -e "kvm\nvhost_net" | sudo tee -a /etc/modules-load.d/kubevirt.conf

If your nodes run containerd, which is K3s’ default, no additional container runtime configuration is required. Containerd is fully supported by KubeVirt.

Step 3. Deploy KubeVirt on the Cluster

At this point, pull the latest KubeVirt release tag and apply the operator and custom resource:

export RELEASE=$(curl -s https://storage.googleapis.com/kubevirt-prow/release/kubevirt/kubevirt/stable.txt)
kubectl apply -f https://github.com/kubevirt/kubevirt/releases/download/${RELEASE}/kubevirt-operator.yaml
kubectl apply -f https://github.com/kubevirt/kubevirt/releases/download/${RELEASE}/kubevirt-cr.yaml

Wait for the KubeVirt K3s virtual machines Kubernetes system to report ready:

kubectl -n kubevirt wait kv kubevirt --for condition=Available --timeout=300s
kubectl get pods -n kubevirt

You should see the following pods in Running state:

virt-api, virt-controller, virt-handler, and virt-operator

If virt-handler crashes with the following error, it’s an AppArmor profile blocking the QEMU binary:

libvirt: error : cannot execute binary /usr/libexec/qemu-kvm

You can remove the host’s libvirtd package or add an exec rule to /etc/apparmor.d/usr.sbin.libvirtd.

Enable Software Emulation as a Fallback

If a node truly has no hardware virtualization support, KubeVirt can still run VMs using software emulation instead, but it will be slower. To enable it:

kubectl patch kubevirt kubevirt -n kubevirt --type merge \
  --patch '{"spec":{"configuration":{"developerConfiguration":{"useEmulation":true}}}}'

Note: Only use this for testing. Production KubeVirt K3s virtual machines Kubernetes deployments should always run on KVM-capable hardware.

Step 4. Install virtctl

virtctl is the CLI extension for VM lifecycle actions, including start, stop, console, VNC, and migrate, that kubectl doesn’t natively support. Install the binary matching the KubeVirt release you deployed:

ARCH=$(uname -s | tr A-Z a-z)-$(uname -m | sed 's/x86_64/amd64/')
curl -L -o virtctl \
  "https://github.com/kubevirt/kubevirt/releases/download/${RELEASE}/virtctl-${RELEASE}-${ARCH}"
chmod +x virtctl
sudo install virtctl /usr/local/bin
virtctl version

Every KubeVirt K3s virtual machines Kubernetes operation from here, such as console access, VM start/stop, port-forwarding, and migration triggers, goes through this binary.

Step 5. Install CDI for Cloud Image Imports

The Containerized Data Importer (CDI) turns a cloud image URL into a bootable PVC via a DataVolume object. This is how you’ll get a real Ubuntu or Fedora disk into your KubeVirt K3s virtual machines Kubernetes cluster instead of building a disk from scratch.

Install the latest CDI release:

export CDI_VERSION=$(basename $(curl -s -w %{redirect_url} \
  https://github.com/kubevirt/containerized-data-importer/releases/latest))
kubectl create -f https://github.com/kubevirt/containerized-data-importer/releases/download/${CDI_VERSION}/cdi-operator.yaml
kubectl create -f https://github.com/kubevirt/containerized-data-importer/releases/download/${CDI_VERSION}/cdi-cr.yaml
kubectl get cdi cdi -n cdi

Wait until the CR’s Phase shows Deployed.

Step 6. Create First Linux VM from a Cloud Image

This is the core workflow for KubeVirt K3s virtual machines Kubernetes. You should define a DataVolume sourced from a public cloud image, then a VirtualMachine object that boots from it.

On your K3s server, create the file with a text editor:

nano linux-vm.yaml

Add:

apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
  name: ubuntu-cloud-disk
  namespace: default
spec:
  source:
    http:
      url: "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img"
  storage:
    accessModes:
      - ReadWriteOnce
    resources:
      requests:
        storage: 20Gi
---
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: legacy-linux-vm
  namespace: default
spec:
  running: true
  template:
    metadata:
      labels:
        kubevirt.io/domain: legacy-linux-vm
    spec:
      domain:
        cpu:
          cores: 2
        resources:
          requests:
            memory: 4Gi
        devices:
          disks:
            - name: rootdisk
              disk:
                bus: virtio
            - name: cloudinitdisk
              disk:
                bus: virtio
          interfaces:
            - name: default
              masquerade: {}
      networks:
        - name: default
          pod: {}
      volumes:
        - name: rootdisk
          dataVolume:
            name: ubuntu-cloud-disk
        - name: cloudinitdisk
          cloudInitNoCloud:
            userData: |
              #cloud-config
              user: ubuntu
              password: changeme
              chpasswd: { expire: False }
              ssh_pwauth: True

Apply it to the cluster and watch the import progress:

kubectl apply -f linux-vm.yaml
kubectl get datavolume ubuntu-cloud-disk -w
kubectl get vmi legacy-linux-vm

The cpu.cores and memory values you set here work just like normal Kubernetes resource requests. That means Kubernetes treats the VM the same way it treats any container pod when deciding where to place it on your nodes.

This is one of the biggest advantages over a standalone hypervisor: you get the same scheduling and resource management for VMs that you already have for containers.

Step 7. Storage Configuration for VMs

The VM’s root disk uses regular Kubernetes storage. Any StorageClass with ReadWriteOnce works fine, or ReadWriteMany if you want to migrate the VM later.

K3s comes with a default storage option called local-path-provisioner. It’s fine for testing on a single node, but it locks the disk to that one node, so the VM can’t move. If you’re running multiple nodes and want to migrate VMs between them, use network storage instead, like Longhorn, Rook-Ceph, or an NFS-based option.

On your K3s server, create a new file:

nano extra-data-disk.yaml

Add:

apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
  name: extra-data-disk
spec:
  source:
    blank: {}
  storage:
    accessModes: ["ReadWriteOnce"]
    resources:
      requests:
        storage: 10Gi

Apply it:

kubectl apply -f extra-data-disk.yaml

This creates a second, empty 10Gi disk. To actually attach it to your VM, add it to the volumes and disks sections of your VM’s YAML file, then reapply that file.

Step 8. Connect to VMs Using virtctl

By default, the VM above uses masquerade pod networking, giving it outbound connectivity through the pod’s IP without extra CNI plugins, which is sufficient for most KubeVirt K3s virtual machines Kubernetes single-VM scenarios.

For SSH or admin access, expose the VM as a Service:

virtctl expose vmi legacy-linux-vm --name=legacy-vm-ssh --port=2222 --target-port=22 --type=NodePort
kubectl get svc legacy-vm-ssh

Or skip the Service entirely and port-forward straight through virtctl:

virtctl port-forward legacy-linux-vm 2222:22
ssh -p 2222 ubuntu@localhost

If you need a console instead of SSH, you can use:

virtctl console legacy-linux-vm

If you want to connect the VM to more than one isolated network, like putting it directly on a VLAN, you’ll need to install Multus CNI separately; KubeVirt K3s virtual machines Kubernetes don’t ship a multi-network plugin by default.

What You Need for Live Migration

Live migration lets a running VM move between nodes without downtime, but it only works if all of these conditions are met simultaneously:

  • The VM’s disk must be on shared and network storage, ReadWriteMany access mode. Local-path PVCs cannot migrate.
  • All nodes must run compatible CPU models, or the VM must request a portable CPU model instead of host-passthrough.
  • The LiveMigration feature must be enabled in the KubeVirt CR, which is enabled by default in current releases.
  • Enough free CPU and memory space must exist on a target node to receive the VM.

Run a migration manually to test the setup:

virtctl migrate legacy-linux-vm
kubectl get vmim

Note: If your cluster is single-node, live migration simply isn’t possible, no matter how you configure it. Keep it in mind before promising anyone high availability for your VMs.

VM Resource Requests and Backup Boundaries

Unlike containers, VMs need their memory request set close to their memory limit. With containers, you can often get away with requesting less than the limit, since only one process might get killed if memory runs out. With a VM, doing the same thing risks crashing the entire guest operating system, not just one process.

Open the Linux VM YAML file:

nano linux-vm.yaml

Under the resources block, set requests and limits close together for predictable VM behavior:

resources:
  requests:
    memory: 4Gi
    cpu: "2"
  limits:
    memory: 4Gi
    cpu: "2"

On backups, tools like Velero can back up the storage disk behind your VM, but that’s not the same as a proper VM backup. They just copy the raw disk data; they don’t coordinate with the VM first to make sure everything is in a safe state before the backup happens.

Hypervisor tools like Proxmox Backup Server do this coordination by talking to an agent inside the VM. Without that, your backup could capture the disk mid-write, which risks corruption if you ever need to restore it.

Choose Between KubeVirt and a Dedicated Hypervisor

KubeVirt isn’t the right tool for every situation. The table below gives a quick way to check if your use case fits, comparing when KubeVirt on K3s makes sense and when a dedicated hypervisor like Proxmox is the smarter pick.

ScenarioBetter Fit
One legacy Windows/Linux app alongside a container platformKubeVirt K3s virtual machines Kubernetes
Dozens of general-purpose VMs, minimal container workloadsDedicated Proxmox host
Need GitOps-managed VM lifecycle alongside microservicesKubeVirt K3s virtual machines Kubernetes
Heavy reliance on hypervisor-native backup/snapshot toolingProxmox / VMware
Multi-node cluster with shared storage already in placeKubeVirt K3s virtual machines Kubernetes (migration-ready)

Conclusion

KubeVirt lets your K3s cluster run a few VMs alongside your containers, using the same kubectl tools you already know; no need to set up a separate hypervisor just for one legacy app. It works best when your goal is keeping everything in one place, not running lots of VMs. For large-scale VM needs, a dedicated Proxmox host is still the better and more backup-friendly choice.

We hope you enjoy this guide. For more information, check the official KubeVirt user guide.

FAQs

Does KubeVirt replace Proxmox or VMware?

No. It’s built for running a few VMs next to your containers on the same cluster, not for large-scale virtualization.

Can KubeVirt run without hardware virtualization?

Yes, via software emulation, but performance is too slow for production use.

Can I live-migrate a VM on a single-node K3s cluster?

No. Live migration requires at least two nodes and shared storage.

Post Your Comment

PerLod delivers high-performance hosting with real-time support and unmatched reliability.

Contact us

Payment methods

payment gateway
Perlod Logo
Privacy Overview

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.