Installing headless KVM VM on Ubuntu 24.04 + Home Assistant OS (HAOS): From “no network” to a clean VM — with fixes along the way

All steps were done on Ubuntu 24.04.2 LTS (headless) with libvirt/KVM and a bridged network. I end with a repeatable, EFI‑booting HAOS VM and a backup plan.

  • I use systemd‑networkd + Netplan bridge (br0 ) with a static IP on the host.
  • I install KVM/QEMU/Libvirt + OVMF (UEFI).
  • I import HAOS qcow2 and create the VM with UEFI, VirtIO (disk & NIC), bridge=br0, and VNC.
  • The HAOS CLI is on the graphical console (tty1) → I use VNC (or add a serial console explicitly).

HAOS requires EFI to boot.


1) Host networking: robust bridge for KVM

1.1 Identify the right NIC

If I have two physical ports, I make sure the cable is in the NIC I bridge. Check link:

ip link
# Look for RUNNING/LOWER_UP on e.g. enp2s0

1.2 Netplan bridge with static host IP

Create /etc/netplan/01-netcfg.yaml :

network:
  version: 2
  renderer: networkd
  ethernets:
    enp2s0:          # <-- my active NIC
      dhcp4: no
  bridges:
    br0:
      interfaces: [enp2s0]
      addresses:
        - 192.168.0.10/24
      nameservers:
        addresses: [192.168.0.1, 8.8.8.8]
      routes:
        - to: default
          via: 192.168.0.1
      parameters:
        stp: false
        forward-delay: 0

Apply & verify:

sudo netplan --debug generate
sudo netplan apply
ip a show br0
ip route
ping 192.168.0.1

Common pitfalls & fixes

  • br0 shows NO-CARRIER : wrong NIC in bridge or cable in the other port. Fix interfaces: [<nic>] and/or move the cable.
  • NetworkManager slows boot / interferes: disable it on servers.
sudo systemctl disable --now NetworkManager

2) Install KVM + libvirt + OVMF (UEFI)

sudo apt update
sudo apt install -y qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils virtinst ovmf
sudo usermod -aG libvirt,kvm $USER
newgrp libvirt

egrep -c '(vmx|svm)' /proc/cpuinfo     # >0 means HW virtualization
systemctl status libvirtd

3) Get the HAOS qcow2

sudo mkdir -p /var/lib/libvirt/images/hassos
cd /var/lib/libvirt/images/hassos

wget https://github.com/home-assistant/operating-system/releases/download/16.2/haos_ova-16.2.qcow2.xz
unxz haos_ova-16.2.qcow2.xz

sudo chown libvirt-qemu:kvm haos_ova-16.2.qcow2
sudo chmod 0644 haos_ova-16.2.qcow2

4) Create the VM (EFI, VirtIO, Bridge, VNC)

virt-install \
  --name homeassistant \
  --description "Home Assistant OS" \
  --memory 4096 --vcpus 2 \
  --disk path=/var/lib/libvirt/images/hassos/haos_ova-16.2.qcow2,format=qcow2,bus=virtio \
  --network bridge=br0,model=virtio \
  --graphics vnc,listen=127.0.0.1 --video virtio \
  --import \
  --boot uefi \
  --osinfo detect=on,require=off
  • Why EFI? HAOS won’t boot without EFI/UEFI.
  • Why VNC? HAOS’s interactive ha CLI is on the graphical console (tty1), not on the serial console.

5) Open the VNC console (securely)

virsh vncdisplay homeassistant
# Example: :0  → port 5900 on the host

ssh -L 5900:127.0.0.1:5900 adm-simon@ha-server
vncviewer localhost:5900

You might need to install vncviewer on your client computer.


6) Give HAOS a fixed IP (inside the guest)

While connected with vncviewer to the homeassistant VM:

login
ha network info
ha network update enp0 ipv4.method static \
  ipv4.address 192.168.0.50/24 \
  ipv4.gateway 192.168.0.1 \
  ipv4.dns 192.168.0.1
ha network apply

Then open http://192.168.0.50:8123 .


7) Problems I hit — and how I fixed them

7.1 Host loses SSH after netplan apply

  • Wrong NIC in bridge → fix interfaces: [<nic>] .
  • Use routes: instead of deprecated gateway4 .
  • Disable NetworkManager.

7.2 DHCP reservation ignored after bridging

  • Reservation tied to physical NIC MAC, but bridge uses its own MAC.
  • Fix: static IP for br0 .

7.3 br0 is NO-CARRIER

  • Cable in wrong port → move cable or change NIC in YAML.

7.4 Slow boot: systemd-networkd-wait-online

  • Limit to br0 :
sudo systemctl edit systemd-networkd-wait-online.service
[Service]
ExecStart=
ExecStart=/lib/systemd/systemd-networkd-wait-online --interface=br0 --timeout=10

7.5 Desktop kept suspending the server

Don’t ask me why, but while trying around with virsh, at some point I decided to install a UI on my server in order to use the UI of KVM instead of command line. Bad idea on a server…

  • Remove GNOME after network is stable:
sudo systemctl set-default multi-user.target
sudo apt purge -y gdm3 gnome-shell gnome-session gnome-control-center gnome-terminal xorg* lightdm*
sudo apt autoremove --purge -y

7.6 virsh console shows nothing

  • HAOS CLI is on VGA (tty1), not serial → use VNC.

7.7 VM doesn’t appear in DHCP/ARP

  • HAOS didn’t boot (no EFI) or NIC model mismatch.
  • Fix: enable EFI and use VirtIO NIC.

7.8 virt-install errors

  • OS name required → add --osinfo detect=on,require=off or --os-variant generic .
  • Size must be specified → qcow2 path wrong or file not decompressed.
  • q35 XML error → change <controller model='pci-root'/> to pcie-root and add VirtIO-SCSI.

8) Backup & Disaster Recovery

Backup

virsh shutdown homeassistant
virsh dumpxml homeassistant > /backup/homeassistant.xml
tar czf /backup/homeassistant.qcow2.tgz /var/lib/libvirt/images/hassos/haos_ova-16.2.qcow2
sudo cp /etc/netplan/*.yaml /backup/
sudo cp -r /etc/libvirt /backup/libvirt-config
rsync -avz /backup/ user@nas:/srv/backup/ha-server/

Restore

sudo apt install -y qemu-kvm libvirt-daemon-system libvirt-clients bridge-utils virtinst ovmf
sudo cp /backup/*.yaml /etc/netplan/
sudo netplan apply
sudo cp -r /backup/libvirt-config/* /etc/libvirt/
sudo cp /backup/homeassistant.qcow2 /var/lib/libvirt/images/hassos/
virsh define /backup/homeassistant.xml
virsh start homeassistant

Key Lessons Learned for me

  • EFI/OVMF is mandatory for HAOS.
  • Use VNC for initial setup; serial console won’t show ha CLI.
  • VirtIO for disk and NIC = better performance.
  • Automate backups: qcow2 + XML + netplan.

Happy automating!

2 Likes

Hi there I have tried your guide and it turned out successful for me in my mac mini running ubuntu server. I really appreciate your work and sharing it here. I was wondering if you have tried installing this in ubuntu desktop with wifi. I have tried and was able to run the VM but when it came to some addons like home assistant matter hub, it was not able to connect to the devices. Hope you would share your experience.

1 Like

When you say “devices” are you referring to Matter/Thread devices, or are you referring to USB Thread sticks?

I am referring to this addon GitHub - t0bst4r/home-assistant-matter-hub: Publish your Home-Assistant Instance using Matter. which doesn’t need any usb devices.

Problem with USB passthrough to homeassistant VM & FIX

FTDI FT232R USB chip (inside the F14USB-GW) periodically resets itself on the USB bus. When it comes back, Linux assigns it a new device number (2 → 4 → 5 → …). QEMU’s USB passthrough is then pointing at the old device number, so the VM looses the device. Meanwhile, the host’s ftdi_sio kernel driver claims the re-appeared device before QEMU can.

Fix 1: Blacklist ftdi_sio on the host

echo "blacklist ftdi_sio" | sudo tee /etc/modprobe.d/blacklist-ftdi.conf

Creates a config file that tells the Linux module loader: “never automatically load the ftdi_sio driver.” This driver provides serial port access (ttyUSB0) on the host — but you don’t need that because the VM accesses the device directly via USB passthrough. Without this blacklist, every time the FTDI re-enumerates, ftdi_sio races QEMU to claim it — and usually wins.

sudo update-initramfs -u

Rebuilds the initial boot filesystem (initramfs) that Linux loads into RAM during early boot. The blacklist config gets baked into it, ensuring ftdi_sio stays blacklisted even if the kernel tries to load it very early in the boot process.

sudo rmmod ftdi_sio

Immediately unloads the already-running ftdi_sio driver from memory. rmmod = “remove module.” This was needed because the blacklist only takes effect on next boot — the driver was already loaded from the current boot.

Fix 2: Disable USB autosuspend

echo 'ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="0403", ATTR{idProduct}=="6001", ATTR{power/autosuspend}="-1"' | \
  sudo tee /etc/udev/rules.d/99-ftdi-no-autosuspend.rules

Creates a udev rule — udev is the Linux subsystem that manages device events (plug, unplug, etc.). This rule says:

  • When a USB device is added (ACTION=="add")
  • And its vendor ID is 0403 (FTDI) with product ID 6001 (FT232R)
  • Set its power/autosuspend attribute to -1 (= never suspend)

By default, Linux suspends idle USB devices after ~2 seconds to save power. The FT232R is notorious for not waking up properly, which triggers a USB reset. -1 disables this entirely for this specific device.

Fix 3: Automatic USB reattach script

The script (/usr/local/bin/ftdi-reattach-vm.sh):

sleep 2

Waits 2 seconds for the USB device to stabilize after re-enumeration.

for d in /sys/bus/usb/drivers/ftdi_sio/*/; do
    [ -e "$d" ] && echo "$(basename $d)" > /sys/bus/usb/drivers/ftdi_sio/unbind 2>/dev/null
done

If ftdi_sio somehow still loaded and claimed the device, this forcefully unbinds it. Writing a device’s interface name (e.g., 1-4:1.0) to the driver’s unbind file tells the kernel: “release this device from this driver.”

virsh detach-device homeassistant /dev/stdin <<'EOF'
<hostdev mode='subsystem' type='usb' managed='yes'>
  <source>
    <vendor id='0x0403'/>
    <product id='0x6001'/>
  </source>
</hostdev>
EOF

virsh detach-device sends a command to libvirt/QEMU: “remove the USB device with vendor 0403 / product 6001 from the VM.” This clears the stale reference that points to the old device number.

virsh attach-device homeassistant /dev/stdin <<'EOF'
<hostdev mode='subsystem' type='usb' managed='yes'>
  <source>
    <vendor id='0x0403'/>
    <product id='0x6001'/>
  </source>
</hostdev>
EOF

Immediately re-attaches the FTDI by vendor/product ID. Libvirt looks up the current device number from the host USB bus and passes it through to the VM. The VM sees a fresh USB device appear, udev inside HAOS creates usb-FTDI_FT232R_USB_UART_AQ03FP3J-if00-port0, and the Eltako integration reconnects.

The udev trigger (/etc/udev/rules.d/99-ftdi-vm-reattach.rules):

ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="0403", ATTR{idProduct}=="6001", RUN+="/bin/bash -c '/usr/local/bin/ftdi-reattach-vm.sh &'"

Every time the FTDI appears on the USB bus (initial boot or after a re-enumeration), udev automatically runs the reattach script in the background (&). This makes the whole recovery automatic — no manual intervention needed.

Summary of the layers

Layer What it does Why needed
Blacklist ftdi_sio Prevents host from claiming the serial device So QEMU can grab it instead
Disable autosuspend Prevents Linux from power-suspending the USB device Reduces the frequency of FTDI resets
Udev reattach script Automatically re-passes the device to the VM after a reset Self-healing when disconnects still happen
Powered USB hub (pending) Clean power + signal regeneration Eliminates the physical disconnects at the source