Four hours. That’s how long I spent trying to migrate a Gentoo VM from my workstation’s KVM to the Proxmox server. Kernel panics. Garbled graphics. UEFI boot entries that vanished between hypervisors. Each fix revealed another problem, and each problem was more architectural than the last.
Then I ran fdisk -l on my main NVMe drive and the entire four hours became irrelevant. 812 gigabytes of unallocated space. Just sitting there. On the drive I use every day.
Sometimes the solution isn’t fixing the problem. It’s finding a better problem.
The Setup
I’d been building Argo OS — my custom Gentoo distribution — inside a KVM virtual machine on my daily driver workstation. It worked perfectly. Full KDE Plasma desktop, all my tools, exact configuration. But running a VM on your daily driver means competing for resources. The VM wanted CPU. The host wanted CPU. Nobody was happy.
Simple plan: migrate the VM to the Proxmox server. Free up the workstation. Let the VM have its own dedicated resources. Done in an afternoon.
The afternoon had other plans.
Problem 1: The Import That Doesn’t Attach
Step 1: Copy the QCOW2 disk image to Proxmox.
rsync -avP gentoo-btrfs.qcow2 [email protected]:/tank/vms/
qm disk import 200 /tank/vms/gentoo-btrfs.qcow2 tank
Step 2: Boot the VM.
PXE-E53: No boot filename received
Booting from Hard Disk...
Blinking cursor. Nothing.
Classic Proxmox gotcha: qm disk import imports the disk but does not attach it to the VM. The disk exists in storage. The VM doesn’t know about it. So it tries PXE boot, fails, tries the hard disk that isn’t connected, and sits there blinking at you.
qm config 200
# boot: order=net0
# unused0: tank:vm-200-disk-0 <-- there it is, "unused"
qm set 200 --scsi0 tank:vm-200-disk-0
qm set 200 --boot order=scsi0
Twenty minutes to figure out what “unused0” meant. The naming is accurate but unhelpful when you’re expecting your disk to just… be there.
Problem 2: UEFI Variables Don’t Travel
Rebooted. Still hung at “Booting from Hard Disk…”
qm config 200 | grep -i "bios\|efi"
# Nothing
The VM was UEFI-based. Proxmox defaults to SeaBIOS. UEFI boot entries live in firmware NVRAM — they’re not on the disk. They’re in the virtual firmware chip. When you move a UEFI VM to a new hypervisor, the boot entries stay behind. You arrive at the new machine with a disk that expects UEFI and firmware that’s running BIOS.
qm set 200 --efidisk0 tank:vm-200-efi,efitype=4m,pre-enrolled-keys=1
qm set 200 --bios ovmf
This one I should have caught immediately. UEFI is explicitly configured per-VM in Proxmox. The default is BIOS. If your source VM was UEFI, you need to tell the destination. There’s no auto-detection, no warning, no “hey, this disk has an EFI System Partition, maybe you want UEFI.” Just silence and a blinking cursor.
Problem 3: Modern Art Boot Screen
GRUB loaded. Progress. I selected the kernel entry and waited.
The screen became modern art. Garbled colors. Corrupted text. Random pixels in patterns that would be interesting in a gallery and are terrifying in a terminal.
I tried everything:
nomodesetkernel parameter — still garbledvga=off— different kind of garbled- Every display type Proxmox offers — garbled in new and creative ways
- Serial console fallback — no output at all
This wasn’t a configuration problem. This was a hardware abstraction mismatch.
The Gentoo kernel was compiled for KVM’s virtual hardware. Specific virtio devices, specific ACPI tables, specific CPU flags. Proxmox’s QEMU presents different virtual hardware. Different emulation, different device IDs, different everything. The kernel was looking for hardware that didn’t exist and trying to initialize hardware it didn’t understand.
I could spend days recompiling kernels with different driver combinations, testing each one, gradually narrowing down the incompatibility. Or I could step back and ask a different question.
The Pivot
“Is this how distro developers would do it?”
No. Real distro developers don’t migrate VM images between hypervisors. They:
- Build reproducibly from source
- Use binary package repositories
- Version control configurations
- Deploy fresh to each target
I was treating this VM image like a pet. Named it. Nurtured it. Spent four hours trying to keep it alive in a new environment. I should have been treating it like cattle. Disposable. Replaceable. Defined by its configuration, not its disk image.
The VM migration was the wrong approach to the right problem.
The Discovery
While planning a fresh bare-metal install on my workstation — the new approach, the right approach — I ran fdisk -l to understand my partition layout.
Device Start End Sectors Size Type
nvme0n1p1 2048 1050623 1048576 512M EFI System
nvme0n1p2 1050624 1083391 32768 16M Linux filesystem
nvme0n1p3 1083392 735160319 734076928 350G Microsoft basic data
nvme0n1p4 735160320 1258143743 522983424 249.4G Microsoft basic data
nvme0n1p5 1258143744 1259458559 1314816 642M Windows recovery
nvme0n1p6 1259458560 1259491327 32768 16M Microsoft reserved
nvme0n1p8 2963310592 3907029134 943718543 450G Linux filesystem
I stared at this output for a while. Something was wrong. Or rather, something was missing.
p6 ends at sector 1,259,491,327. p8 starts at sector 2,963,310,592. That’s a gap of 1,703,819,265 sectors. At 512 bytes per sector, that’s…
812 gigabytes. Unallocated. On my primary NVMe drive.
I’d been so focused on the 450GB openSUSE partition and the Windows partitions that I never looked at the gaps between them. 812GB of raw, unformatted, unpartitioned space. On the drive I boot from every single day. Right there in the partition table, visible to anyone who bothered to run fdisk -l.
I never bothered.
The New Plan
Forget the VM migration. Forget the hypervisor compatibility issues. Fresh Gentoo install on bare metal, in the 812GB gap I didn’t know I had.
Proposed layout in the 812GB gap:
|- p7: 200G Gentoo root (/)
|- p9: 8G Gentoo swap
+- Remaining: ~604GB (future expansion)
Why this is dramatically better than the VM approach:
- Native hardware. The NVIDIA GPU works without passthrough headaches. The CPU runs at full speed without hypervisor overhead. Real hardware, real drivers, real performance.
- No hypervisor compatibility. The kernel is compiled for this exact machine. No abstraction layer mismatch. No garbled graphics.
- Binary packages from the Tau-Beta server at
10.42.0.194. The workstation never compiles. Everything arrives pre-built. - All configurations in Git. Deploy fresh, pull configs, install packages. Reproducible in minutes.
Four hours of VM migration debugging, solved by discovering that I had more free space than I was using in the first place.
The Architecture Shift
This wasn’t just a “where does Gentoo live” decision. It changed the whole development model.
Before:
+-------------------------------------+
| Workstation |
| |- openSUSE (daily driver) |
| +- KVM VM (Gentoo Golden Image) |
| +- Compiles @world |
| +- Eats CPU/RAM |
+--------------------------------------+
The workstation was doing two jobs: being my daily driver AND running a Gentoo build VM. Both competed for the same CPU, same RAM, same I/O. Compilation in the VM made the host sluggish. Using the host made compilation slower.
After:
+-------------------------------------+
| Tau-Beta (Build Server) |
| +- Bare metal Gentoo |
| +- Compiles @world continuously |
| +- Serves binpkgs via HTTP |
+-----------------+--------------------+
|
| HTTP: binary packages
| Git: configurations
v
+-------------------------------------+
| Workstation (Capella-Outpost) |
| |- openSUSE (legacy, shrinking) |
| +- Gentoo (fresh install, 200GB) |
| +- Pulls configs from Git |
| +- Pulls binpkgs from Tau-Beta |
| +- Never compiles locally |
+--------------------------------------+
Clean separation. The Tau-Beta machine compiles. The workstation consumes. No resource competition. No hypervisor overhead. Binary package installation instead of hours of compilation:
# On workstation - configure to consume packages
echo 'PORTAGE_BINHOST="http://10.42.0.194"' >> /etc/portage/make.conf
echo 'FEATURES="${FEATURES} getbinpkg"' >> /etc/portage/make.conf
# Install using binary packages - minutes, not hours
emerge -av --getbinpkg @world
This architecture came directly from the failed VM migration. If the migration had worked, I’d still be running a VM on my daily driver, competing for resources, treating disk images like pets. The failure forced a better design.
What Four Hours of “Failure” Actually Produced
I didn’t migrate a VM. I designed an architecture.
The VM migration taught me that hypervisor compatibility is non-trivial. UEFI variables, hardware abstraction layers, compiled-in drivers — they’re all tied to the source environment. Moving a VM between hypervisors is moving a system that was built for one set of virtual hardware to a completely different set. It can work. It’s never clean.
The fdisk discovery taught me to check assumptions before solving problems. I assumed my disk was full. I assumed I needed to shrink partitions or add storage. 812GB said otherwise. The assumption was wrong, and I’d been working around it for months.
The architecture redesign taught me to think like a distro developer, not a sysadmin. Binary package repositories. Git-managed configurations. Reproducible deployments. These patterns exist because VM images don’t scale. Disk images are pets; configurations are cattle.
The bare metal decision taught me that sometimes the simplest answer is the best one. No hypervisor. No abstraction layer. No compatibility matrix. Just a partition, a filesystem, and a bootloader. The stuff Linux was designed to do.
The Irony
I spent four hours trying to fix a VM migration. The solution was already on my disk, in the form of 812 gigabytes of free space I forgot existed.
The “failure” — garbled graphics, kernel panics, UEFI nightmares — wasn’t wasted time. It was the motivation to look more carefully at what I already had. Without the frustration of the migration, I never would have run fdisk -l on my main drive. I never would have found the gap. I never would have redesigned the architecture.
Sometimes the bug isn’t in the code. It’s in the assumption that you understood the problem.
I assumed I needed to migrate a VM. I actually needed to look at my own partition table. The four hours of debugging were expensive, but the architecture they produced was worth it.
The 812GB is still mostly empty. 604GB of future expansion, waiting for whatever comes next. But the 200GB Gentoo partition that came out of that discovery runs faster, cleaner, and simpler than the VM ever did.
All because I finally ran fdisk -l.