Bare-Metal to VM: A Pragmatic P2V Migration to Proxmox

By Anas Semesmieh · June 7, 2026 · Homelab, Proxmox, Networking, Self-Hosting

My homelab runs 24+ Docker services on a single Samsung 870 EVO 500 GB SSD — reverse proxies, DNS, SSO, password management, monitoring, media, and more. All of it on bare metal, one kernel panic away from a full outage with no isolation between workloads. This post is about how I moved that entire stack into a Proxmox VE virtual machine without downtime, and why the migration itself is just the beginning of a larger architecture shift.

This wasn't a clean-room reinstall. The goal was to lift the running system — disk, services, networking — into a VM with zero rebuilds. Pragmatism over perfection.

The Failed Plan: Clonezilla Cold-Clone

The original approach was textbook: boot Clonezilla, image the SSD to an NFS target on the Synology NAS, then restore into a fresh VM disk. Simple in theory.

# Clonezilla boot parameters
ocs-live-restore restoredisk ask_user sda
# Target: NFS share on Synology
mount -t nfs 10.0.0.10:/volume1/docker/clonezilla-images /home/partimag

It failed immediately. The NFS export permissions on the Synology blocked Clonezilla's write attempts — the squashfs live environment runs as a UID that doesn't map cleanly to Synology's NFS permission model. I could have debugged the NFS all_squash and anonuid mappings, but that was a rabbit hole with no guaranteed outcome and a server sitting offline.

Time to pivot.

The Pivot: Direct SSD Passthrough

Instead of imaging and restoring, I passed the physical SSD directly to the VM using its stable /dev/disk/by-id/ path. The existing Ubuntu installation boots as-is — no cloning, no filesystem copy, no intermediate image.

# Create OVMF (UEFI) VM with q35 chipset
qm create 100 --name homelab-recovery --memory 14336 --cores 8 \
  --bios ovmf --machine q35 \
  --efidisk0 local-lvm:1,efitype=4m,pre-enrolled-keys=0 \
  --ostype l26 --cpu host

# Attach the physical SSD by stable disk ID
qm set 100 --sata0 /dev/disk/by-id/ata-Samsung_SSD_870_EVO_500GB_S6PXNX0W614894K

# Set boot order
qm set 100 --boot order=sata0

# Start the VM
qm start 100

The VM booted into the existing Ubuntu 26.04 LTS installation on first try. All Docker containers, systemd services, and configs were intact. The kernel (7.0.0-22-generic) came up cleanly under QEMU/KVM with no driver issues.

Preparing Proxmox: IOMMU, Networking, and Repository Cleanup

Before creating the VM, the Proxmox VE 9.2.3 host (kernel 6.8.12-9-pve → upgraded to 7.0.6-2-pve) needed a few preparations.

IOMMU Passthrough

Enabled IOMMU in GRUB for potential future PCIe passthrough:

# /etc/default/grub
GRUB_CMDLINE_LINUX_DEFAULT="quiet intel_iommu=on iommu=pt"
update-grub && reboot

Enterprise Repository Cleanup

Proxmox 9.x uses deb822 .sources format. Disabled the enterprise repos that require a subscription:

# Disable enterprise repos (no subscription)
mv /etc/apt/sources.list.d/pve-enterprise.sources \
   /etc/apt/sources.list.d/pve-enterprise.sources.disabled

mv /etc/apt/sources.list.d/ceph.sources \
   /etc/apt/sources.list.d/ceph.sources.disabled

# Full system upgrade
apt update && apt dist-upgrade -y

Dual-Interface Architecture

The homelab needs two isolated networks: a LAN for service traffic and a dedicated NAS link for storage I/O. On bare metal this was two physical NICs. Under Proxmox, it becomes two Linux bridges.

Host Bridge Configuration

# /etc/network/interfaces (Proxmox host)
auto vmbr0
iface vmbr0 inet static
    address 192.168.20.99/24
    gateway 192.168.20.1
    bridge-ports lan
    bridge-stp off
    bridge-fd 0

auto vmbr1
iface vmbr1 inet static
    address 10.0.0.30/24
    bridge-ports usbcadptr
    bridge-stp off
    bridge-fd 0
    mtu 9000

VM NIC Assignment

# Attach both networks to VM 100
qm set 100 --net0 virtio=BC:24:11:49:0F:83,bridge=vmbr0
qm set 100 --net1 virtio=BC:24:11:DD:1D:74,bridge=vmbr1,mtu=9000

Guest Netplan (Ubuntu)

# /etc/netplan/00-installer-config.yaml
network:
  version: 2
  ethernets:
    ens18:
      match:
        macaddress: "bc:24:11:49:0f:83"
      addresses:
        - 192.168.20.69/24
      routes:
        - to: default
          via: 192.168.20.1
      nameservers:
        addresses: [1.1.1.1, 9.9.9.9]
    ens19:
      match:
        macaddress: "bc:24:11:dd:1d:74"
      addresses:
        - 10.0.0.20/24
      mtu: 9000

MAC-matching in netplan ensures interface names stay predictable regardless of PCI slot ordering. The guest sees ens18 for LAN and ens19 for NAS without needing fragile set-name rules.

Jumbo Frame Optimization

The NAS link runs at MTU 9000 end-to-end: physical NIC → host bridge → virtio tap → guest interface → NFS client. This reduces per-packet overhead for large sequential transfers (backups, media streaming, bulk reads).

Critical detail: the virtio NIC must have mtu=9000 set explicitly in the QEMU device config. Without it, the guest interface caps at 1500 even if netplan requests 9000:

# This is what made it work — MTU in the QEMU NIC definition
qm set 100 --net1 virtio=BC:24:11:DD:1D:74,bridge=vmbr1,mtu=9000

Verification from inside the guest:

# Confirm MTU 9000 is active on ens19
ip link show ens19 | grep mtu
# 3: ens19: <BROADCAST,MULTICAST,UP> mtu 9000 qdisc fq_codel state UP

# Verify end-to-end jumbo frame path to NAS (8972 + 28 byte header = 9000)
ping -M do -s 8972 -c 3 10.0.0.10
# PING 10.0.0.10 (10.0.0.10) 8972(9000) bytes of data.
# 8980 bytes from 10.0.0.10: icmp_seq=1 ttl=64 time=0.284 ms

NFS Automount Strategy

The guest mounts four NFS shares from the Synology NAS at 10.0.0.10. On bare metal these were hard mounts in /etc/fstab, which caused boot hangs when the NAS interface wasn't ready in time. Under virtualization, the timing problem got worse — the guest's NAS NIC initializes after the mount targets are evaluated.

The fix: x-systemd.automount. The kernel creates the mount point immediately (satisfying any service dependencies), but the actual NFS connection happens lazily on first access.

# /etc/fstab — NFS mounts with systemd automount
10.0.0.10:/volume1/docker    /mnt/nas/docker    nfs4 x-systemd.automount,x-systemd.idle-timeout=600,nofail,_netdev,rsize=1048576,wsize=1048576 0 0
10.0.0.10:/volume1/media     /mnt/nas/media     nfs4 x-systemd.automount,x-systemd.idle-timeout=600,nofail,_netdev,rsize=1048576,wsize=1048576 0 0
10.0.0.10:/volume1/backups   /mnt/nas/backups   nfs4 x-systemd.automount,x-systemd.idle-timeout=600,nofail,_netdev,rsize=1048576,wsize=1048576 0 0
10.0.0.10:/volume1/personal  /mnt/nas/personal  nfs4 x-systemd.automount,x-systemd.idle-timeout=600,nofail,_netdev,rsize=1048576,wsize=1048576 0 0
# Verify automounts are registered
systemctl list-automounts
# UNIT                        LOAD   ACTIVE SUB     DESCRIPTION
# mnt-nas-docker.automount    loaded active waiting /mnt/nas/docker
# mnt-nas-media.automount     loaded active waiting /mnt/nas/media
# mnt-nas-backups.automount   loaded active waiting /mnt/nas/backups
# mnt-nas-personal.automount  loaded active waiting /mnt/nas/personal

The nofail flag prevents boot failure if the NAS is offline. The _netdev flag ensures systemd waits for network-online before attempting the mount. Combined with automount, this is a robust pattern for NFS in virtualized environments.

Automated Backups: vzdump to NAS

With the VM running, Proxmox's built-in backup scheduler handles full VM-level backups. The backup target is an NFS share on the Synology, accessed over the dedicated 10 GbE NAS bridge.

Storage Backend

# /etc/pve/storage.cfg (relevant section)
nfs: nas-backups
    export /volume1/docker/proxmox-backups
    path /mnt/pve/nas-backups
    server 10.0.0.10
    content backup
    prune-backups keep-last=7

Scheduled Job

# /etc/pve/jobs.cfg
vzdump: backup-homelab
    enabled 1
    schedule 0 2 * * *
    storage nas-backups
    vmid 100
    mode suspend
    compress zstd
    prune-backups keep-last=7
    mailnotification failure
    notes-template {{guestname}}-{{node}}-{{vmid}}

Every day at 02:00, the VM is briefly suspended (typically <30 seconds for a 500 GB disk with zstd), a consistent snapshot is written to the NAS, and old backups beyond the last 7 are pruned automatically. Manual runs work identically:

# Manual backup (same parameters as the scheduled job)
vzdump 100 --mode suspend --compress zstd --storage nas-backups

The suspend mode guarantees filesystem consistency without requiring a guest agent. Combined with NAS-level snapshots on the Synology side, this provides two independent recovery paths.

What This Unlocks: The Road Ahead

This migration is not the end state — it's the foundation. Running on bare metal meant every service shared one kernel, one failure domain, and one recovery path. Now that the homelab lives inside Proxmox, the architecture can evolve properly.

Per-Service Migration Analysis

Every one of the 24+ hosted applications will be analyzed individually and migrated to its optimal runtime environment:

Minimal Blast Radius

The driving principle: one failure should never cascade. When a reverse proxy crashes, it shouldn't take down the password manager. When a database OOMs, DNS should keep resolving. Proxmox gives this through hard VM/CT boundaries, independent resource limits via cgroups, and separate kernel namespaces.

# Future: per-VM resource capping
qm set 101 --cpulimit 2 --memory 2048 --balloon 1024
qm set 102 --cpulimit 4 --memory 4096 --swap 0

Maximum Recoverability

With NAS-backed storage, recovery is no longer theoretical — it's guaranteed and tested:

# Snapshot before any risky operation
qm snapshot 100 --name pre-upgrade --description "Before kernel upgrade"

# If something breaks — instant rollback
qm rollback 100 --snapname pre-upgrade

# List all available recovery points
qm listsnapshot 100

Three layers of backup (vzdump daily, on-demand snapshots, NAS-level protection) mean that no single failure — disk, VM, or operator error — results in permanent data loss. The NAS storage makes this sustainable: 7 daily backups of a 500 GB VM compress to roughly 200 GB with zstd, well within the available capacity.

Future Architecture Targets

Lessons Learned

Lesson Detail
Pragmatism wins Direct SSD passthrough was faster, safer, and more reversible than any imaging approach. The server was back online in under 30 minutes.
Plan for rollback The physical SSD is untouched. If the VM fails catastrophically, plug it back into bare metal and boot. Zero-risk migration.
MTU must be explicit everywhere Jumbo frames silently fail if any single hop in the chain defaults to 1500. Verify with ping -M do end-to-end.
Automount solves timing Hard NFS mounts in VMs are a race condition. x-systemd.automount decouples mount availability from boot ordering.
Start monolithic, decompose later Migrating the whole system first preserves uptime. Decomposing into smaller VMs/CTs is the next phase — done safely with snapshots at every step.

Closing Thought

A P2V migration doesn't have to be a multi-day project with imaging tools and careful partition math. Sometimes the right answer is passing a disk straight through and letting the existing OS boot where it lands. The real value isn't in the migration itself — it's in what comes after: isolation, recoverability, and the freedom to evolve each service independently without risking the whole stack.

The server is no longer a single point of failure. It's a platform.