A KubeVirt VM is the heaviest stateful workload you can put on this platform, and this platform is built to replace the ground it stands on. Nodes are treated as cattle: drained and rebuilt on upgrades, rollouts, and repairs. A container tolerates that. A virtual machine holding 8Gi of memory and a disk on local NVMe does not. So before you reach for the manifest, understand what happens the first time its node goes away. ## What happens to your VM when the node drains When a bare-metal node is drained, the VM's pod (the `virt-launcher`) is evicted like any other, and KubeVirt reschedules the VM onto another node, but as a fresh VM, from its disk image. If that disk sat on the node's [local NVMe](/docs/hetzner/apalla/storage/local/local-nvme-with-topolvm), the copy went with the old node: local NVMe is fast but node-pinned. Treat a local-NVMe VM disk as scratch; keep anything you cannot lose on network-backed storage or outside the VM. The setting that gives a heavy VM a graceful exit is the drain timeout: each worker pool waits up to 180 seconds by default before a node is torn down. Raise it per pool via `spec.topology.workers.machineDeployments[].deletion.nodeDrainTimeoutSeconds` so a large VM can flush and shut down cleanly instead of being cut off mid-write. > [!NOTE] > KubeVirt is user-managed upstream software running in your cluster, not a platform component. Pin a specific release rather than installing from `releases/latest/download`, and design around the fact that node churn will reschedule your VMs. ## It runs on bare metal only KubeVirt needs hardware virtualization (KVM). Hetzner bare-metal servers have it; Hetzner Cloud instances lack nested virtualization, so VMs will not run there. If your cluster has no bare-metal nodes, [add a bare-metal pool](/docs/hetzner/apalla/servers-and-nodes/provision/add-bare-metal-servers) first. It is the same KVM requirement as the [secure runtime](/docs/hetzner/apalla/workloads/secure/run-in-the-secure-runtime), and the two pair cleanly: KubeVirt for full virtual machines, the secure runtime for pod-level VM isolation. ## Install and define a VM Install KubeVirt and CDI (the Containerized Data Importer for disk images), pinning a version: ```console $ VERSION=v1.4.0 $ kubectl apply -f https://github.com/kubevirt/kubevirt/releases/download/$VERSION/kubevirt-operator.yaml $ kubectl apply -f https://github.com/kubevirt/kubevirt/releases/download/$VERSION/kubevirt-cr.yaml ``` Then declare a `VirtualMachine`, backing its disk with local NVMe for speed (scratch storage, per the caveat above): ```yaml title="virtualmachine.yaml" apiVersion: kubevirt.io/v1 kind: VirtualMachine metadata: name: builder spec: running: true template: spec: domain: cpu: {cores: 4} resources: requests: {memory: 8Gi} devices: disks: - name: root disk: {bus: virtio} volumes: - name: root dataVolume: {name: builder-root} ``` ## Where it fits and what to reserve Where a container is not enough, a VM earns its weight: isolated CI/CD runners where each build gets a clean machine sealed off from the host and the next build (a common first use); legacy applications that need a full OS or a specific kernel and cannot be containerized; and multi-tenant platforms that give each tenant a strongly isolated VM. A running VM reserves its CPU and memory like a Guaranteed pod, so size the bare-metal pool for the VMs you intend to run plus headroom; otherwise a VM and your containers end up competing for the same cores. For latency-sensitive VMs on a multi-socket server, the same [NUMA alignment](/docs/hetzner/apalla/workloads/specialized/run-numa-aligned-workloads) rules apply.