Swimming Upstream: Exporting VMs from Proxmox to ESXi/vCenter

Everyone is migrating from VMware to Proxmox. Blog posts, Reddit threads, YouTube tutorials — the internet is flooded with guides on escaping VMware licensing costs and embracing the open-source promised land. It’s practically a movement.

And then there’s me, going the other way.

While the rest of the world is celebrating their freedom from vCenter, I found myself needing to export VMs out of Proxmox and into ESXi — because a client requires it. I searched for a proper guide and found almost nothing useful: a few forum posts with single-disk examples, no mention of multiple disks, UEFI, Windows Server 2025, or the OVF errors vCenter throws at you. So here’s the guide I wish existed.

What We’re Building

We’ll export a VM from Proxmox as an OVA file that can be imported directly into ESXi or vCenter. The process involves:

  • Installing ovftool on Proxmox
  • Converting disks to streamOptimized VMDK (only real data, not empty space)
  • Writing a valid OVF descriptor that vCenter actually accepts
  • Packaging everything into a single .ova file

Step 1: Install ovftool on Proxmox

Proxmox doesn’t ship with ovftool, so we need to install it manually. Download VMware-ovftool-5.1.0-25410048-lin.x86_64 from Broadcom’s developer portal and copy it to your Proxmox server via WinSCP or SCP.

Then install unzip and extract it:

apt-get install -y unzip
cd /tmp
unzip VMware-ovftool-5.1.0-25410048-lin.x86_64.zip

Move it to a permanent location and create a symlink:

mv /tmp/ovftool /opt/ovftool
ln -s /opt/ovftool/ovftool /usr/local/bin/ovftool

Verify the installation:

ovftool --version
# VMware ovftool 5.1.0 (build-24031167)

💡 You may see a getcwd: cannot access parent directories warning if you run it from the directory that was just moved. It’s harmless — just cd / first.

Step 2: Check VM Configuration and Actual Disk Usage

Before converting anything, check how much data is actually on the disks. Proxmox uses LVM thin provisioning, which means a 500GB disk might only contain 1GB of real data — and your OVA will reflect that.

qm config 300
scsi0: local-lvm:vm-300-disk-1,size=150G
scsi1: local-lvm:vm-300-disk-2,size=150G
scsi2: local-lvm:vm-300-disk-3,size=500G
scsi3: local-lvm:vm-300-disk-4,size=100G

Now check actual usage per disk:

lvs /dev/pve/vm-300-disk-1
lvs /dev/pve/vm-300-disk-2
lvs /dev/pve/vm-300-disk-3
lvs /dev/pve/vm-300-disk-4
vm-300-disk-1  150.00g  24.68%  → ~37G actual
vm-300-disk-2  150.00g  10.37%  → ~15G actual
vm-300-disk-3  500.00g   0.02%  → ~0.1G actual
vm-300-disk-4  100.00g   0.09%  → ~0.1G actual

In our case: ~52GB of real data instead of 900GB. The OVA will be roughly that size.

Also check where you have free space — avoid /tmp, it’s tmpfs and typically limited to 2GB:

df -h
# /dev/mapper/pve-local--backup  2.9T  2.1M  2.8T  /var/backup  ← use this

Step 3: Stop the VM and Convert Disks

⚠️ Always stop the VM before exporting. Exporting a running VM risks data corruption.

qm stop 300
qm status 300
# status: stopped

Create the export folder and convert each disk to streamOptimized VMDK. This format compresses blocks and skips empty space — it’s designed exactly for transport:

mkdir -p /var/backup/vm-300-export

qemu-img convert -p -f raw -O vmdk \
  -o subformat=streamOptimized \
  /dev/pve/vm-300-disk-1 \
  /var/backup/vm-300-export/vm-300-disk-1.vmdk

qemu-img convert -p -f raw -O vmdk \
  -o subformat=streamOptimized \
  /dev/pve/vm-300-disk-2 \
  /var/backup/vm-300-export/vm-300-disk-2.vmdk

qemu-img convert -p -f raw -O vmdk \
  -o subformat=streamOptimized \
  /dev/pve/vm-300-disk-3 \
  /var/backup/vm-300-export/vm-300-disk-3.vmdk

qemu-img convert -p -f raw -O vmdk \
  -o subformat=streamOptimized \
  /dev/pve/vm-300-disk-4 \
  /var/backup/vm-300-export/vm-300-disk-4.vmdk

💡 Skip the EFI disk and TPM disk — ESXi recreates them automatically on import.

Verify the result:

ls -lh /var/backup/vm-300-export/
# 23G  vm-300-disk-1.vmdk
# 16G  vm-300-disk-2.vmdk
# 70M  vm-300-disk-3.vmdk
# 15M  vm-300-disk-4.vmdk

Step 4: Write the OVF Descriptor

This is the part nobody documents properly. The OVF file is an XML descriptor that tells vCenter what the VM looks like. If the section order is wrong, vCenter will refuse the import with cryptic errors.

The correct order is always: ReferencesDiskSectionNetworkSectionVirtualSystem.

cat > /var/backup/vm-300-export/vm-300.ovf << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<Envelope xmlns="http://schemas.dmtf.org/ovf/envelope/1"
  xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1"
  xmlns:vmw="http://www.vmware.com/schema/ovf"
  xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"
  xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData">
  <References>
    <File ovf:id="disk-1" ovf:href="vm-300-disk-1.vmdk"/>
    <File ovf:id="disk-2" ovf:href="vm-300-disk-2.vmdk"/>
    <File ovf:id="disk-3" ovf:href="vm-300-disk-3.vmdk"/>
    <File ovf:id="disk-4" ovf:href="vm-300-disk-4.vmdk"/>
  </References>
  <DiskSection>
    <Info>Virtual disk information</Info>
    <Disk ovf:diskId="disk-1" ovf:fileRef="disk-1" ovf:capacity="161061273600"
          ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized"/>
    <Disk ovf:diskId="disk-2" ovf:fileRef="disk-2" ovf:capacity="161061273600"
          ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized"/>
    <Disk ovf:diskId="disk-3" ovf:fileRef="disk-3" ovf:capacity="536870912000"
          ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized"/>
    <Disk ovf:diskId="disk-4" ovf:fileRef="disk-4" ovf:capacity="107374182400"
          ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized"/>
  </DiskSection>
  <NetworkSection>
    <Info>List of logical networks</Info>
    <Network ovf:name="VM Network">
      <Description>VM Network</Description>
    </Network>
  </NetworkSection>
  <VirtualSystem ovf:id="d-s9999g">
    <Info>Windows Server 2025 Standard</Info>
    <Name>d-s9999g</Name>
    <OperatingSystemSection ovf:id="112">
      <Info>Guest OS</Info>
      <Description>windows2025srv_64Guest</Description>
    </OperatingSystemSection>
    <VirtualHardwareSection>
      <Info>Virtual hardware</Info>
      <System>
        <vssd:ElementName>Virtual Hardware Family</vssd:ElementName>
        <vssd:InstanceID>0</vssd:InstanceID>
        <vssd:VirtualSystemType>vmx-19</vssd:VirtualSystemType>
      </System>
      <Item>
        <rasd:ElementName>4 vCPU</rasd:ElementName>
        <rasd:InstanceID>1</rasd:InstanceID>
        <rasd:ResourceType>3</rasd:ResourceType>
        <rasd:VirtualQuantity>4</rasd:VirtualQuantity>
        <vmw:CoresPerSocket ovf:required="false">4</vmw:CoresPerSocket>
      </Item>
      <Item>
        <rasd:ElementName>16384 MB RAM</rasd:ElementName>
        <rasd:InstanceID>2</rasd:InstanceID>
        <rasd:ResourceType>4</rasd:ResourceType>
        <rasd:VirtualQuantity>16384</rasd:VirtualQuantity>
        <rasd:AllocationUnits>byte * 2^20</rasd:AllocationUnits>
      </Item>
      <Item>
        <rasd:ElementName>UEFI Boot</rasd:ElementName>
        <rasd:InstanceID>3</rasd:InstanceID>
        <rasd:ResourceType>1</rasd:ResourceType>
        <vmw:Config ovf:required="false" vmw:key="firmware" vmw:value="efi"/>
      </Item>
      <Item>
        <rasd:ElementName>SCSI Controller 0</rasd:ElementName>
        <rasd:InstanceID>4</rasd:InstanceID>
        <rasd:ResourceType>6</rasd:ResourceType>
        <rasd:ResourceSubType>VirtualSCSI</rasd:ResourceSubType>
        <rasd:Address>0</rasd:Address>
      </Item>
      <Item>
        <rasd:ElementName>Hard Disk 1</rasd:ElementName>
        <rasd:HostResource>ovf:/disk/disk-1</rasd:HostResource>
        <rasd:InstanceID>5</rasd:InstanceID>
        <rasd:Parent>4</rasd:Parent>
        <rasd:AddressOnParent>0</rasd:AddressOnParent>
        <rasd:ResourceType>17</rasd:ResourceType>
      </Item>
      <Item>
        <rasd:ElementName>Hard Disk 2</rasd:ElementName>
        <rasd:HostResource>ovf:/disk/disk-2</rasd:HostResource>
        <rasd:InstanceID>6</rasd:InstanceID>
        <rasd:Parent>4</rasd:Parent>
        <rasd:AddressOnParent>1</rasd:AddressOnParent>
        <rasd:ResourceType>17</rasd:ResourceType>
      </Item>
      <Item>
        <rasd:ElementName>Hard Disk 3</rasd:ElementName>
        <rasd:HostResource>ovf:/disk/disk-3</rasd:HostResource>
        <rasd:InstanceID>7</rasd:InstanceID>
        <rasd:Parent>4</rasd:Parent>
        <rasd:AddressOnParent>2</rasd:AddressOnParent>
        <rasd:ResourceType>17</rasd:ResourceType>
      </Item>
      <Item>
        <rasd:ElementName>Hard Disk 4</rasd:ElementName>
        <rasd:HostResource>ovf:/disk/disk-4</rasd:HostResource>
        <rasd:InstanceID>8</rasd:InstanceID>
        <rasd:Parent>4</rasd:Parent>
        <rasd:AddressOnParent>3</rasd:AddressOnParent>
        <rasd:ResourceType>17</rasd:ResourceType>
      </Item>
      <Item>
        <rasd:ElementName>Network Adapter</rasd:ElementName>
        <rasd:InstanceID>9</rasd:InstanceID>
        <rasd:ResourceType>10</rasd:ResourceType>
        <rasd:ResourceSubType>E1000</rasd:ResourceSubType>
        <rasd:Connection>VM Network</rasd:Connection>
      </Item>
    </VirtualHardwareSection>
  </VirtualSystem>
</Envelope>
EOF

Step 5: Build the OVA with ovftool

Now let ovftool do the packaging. It validates the OVF, calculates checksums, and produces a proper OVA:

cd /var/backup/vm-300-export
ovftool vm-300.ovf /var/backup/vm-300.ova
Opening OVF source: vm-300.ovf
Opening OVA target: /var/backup/vm-300.ova
Writing OVA package: /var/backup/vm-300.ova
Transfer Completed
Completed successfully

💡 Warnings about "Wrong file size" and "No manifest" are expected — ovftool corrects them in the final OVA. As long as you see Completed successfully you're good.

ls -lh /var/backup/vm-300.ova
# -rw------- 1 root root 40G Jun 3 14:05 /var/backup/vm-300.ova

40GB instead of 900GB. Not bad.

Step 6: Import into vCenter

Copy the OVA to your local machine via WinSCP, then in vSphere Client:

  1. Right-click on the cluster/host → Deploy OVF Template
  2. Select the .ova file
  3. Follow the wizard — name, datastore, network
  4. At the network step, map "VM Network" to whatever portgroup exists in your environment — it doesn't need to match exactly, vCenter lets you remap it
  5. Choose Thin Provision if storage is limited, or Thick for production performance
  6. Done

Troubleshooting — Errors You Will Hit

Value 'VM Network' of Connection element not found in NetworkSection

Your OVF is missing the <NetworkSection> block, or it's in the wrong position. Add it between DiskSection and VirtualSystem, then rebuild the OVA.

ELEMENT_REQUIRED: Element 'References' expected

The <References> section is not first in the OVF. Fix the order: References → DiskSection → NetworkSection → VirtualSystem.

Need to fix an OVA without rebuilding from scratch?

Extract it with 7-Zip, edit the .ovf in Notepad++, then repackage from PowerShell — note that 7-Zip cannot create a valid OVA, only tar can:

cd C:\Users\you\Desktop\vm-folder
tar cvf C:\Users\you\Desktop\vm-fixed.ova vm.ovf disk1.vmdk disk2.vmdk

⚠️ The OVF must always be the first file in the archive.

Conclusion

Migrating from Proxmox to ESXi isn't as well-documented as the reverse, but it's entirely doable with the right tools and a properly structured OVF. The two things that will save you the most time: use streamOptimized to avoid copying terabytes of empty disk space, and get the OVF section order right the first time so vCenter doesn't reject it.

If you found this useful — or if you're also stubbornly going against the current — drop a comment below.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top