You’ve got a VMware-exported VMDK and a Proxmox cluster that doesn’t care about your nostalgia. The migration is “just a disk conversion,” until it isn’t: the VM boots to a black screen, conversion crawls at 20 MB/s, or qemu-img starts reciting poetry about “invalid VMDK descriptor.”
This is the field guide I wish more people used before running conversions on the only copy of a production disk. It’s blunt on purpose. You’re here to move bytes safely, quickly, and with fewer surprises than a Monday morning change window.
The mental model: what you’re actually converting
“VMDK to QCOW2” sounds like a single-file makeover. In practice, you’re translating a storage container format, its allocation strategy, and often its snapshot history into something QEMU can run efficiently.
VMDK can be:
- Single-file monolithicSparse (common in exports and some backups).
- Two-part: a small descriptor text file plus a big flat extent file (common on ESXi datastores).
- Split into 2GB chunks (older/portable style).
- Snapshot chains (delta disks) where “the disk” is a stack of files that only makes sense together.
QCOW2 is also not “just a file.” It can store snapshots, compression, encryption, and it has metadata that can become a performance tax if you treat it like a raw block device. On Proxmox, you also have a second decision: file-based storage (dir, NFS, CIFS) vs block-based (LVM-thin, ZFS zvol, Ceph RBD). The right target often isn’t QCOW2 at all—it’s raw on a zvol or LVM-thin. But sometimes QCOW2 is exactly what you want, especially for file storage and portability.
Here’s the opinionated rule: convert once, validate twice, then import. If you’re converting inside a random directory with unknown filesystem semantics and no verification, you’re not migrating—you’re gambling.
Interesting facts & historical context (the useful kind)
- VMDK predates modern “cloud image” thinking. It grew up in an era where desktop virtualization and datastores shaped design more than API-first automation.
- VMDK “descriptor + flat” is a feature, not a bug. The tiny descriptor file is effectively metadata and pointers; lose it and you may still salvage the flat extent—if you know geometry and format details.
- QCOW (v1) came before QCOW2. QCOW2 replaced it to improve features like snapshots and refcounting; it’s been the default for years for good reasons.
- qemu-img is older than many production runbooks. It’s a sharp tool; it will happily do exactly what you asked, including irreversible choices.
- “Thin provisioning” means different things across layers. A thin VMDK can become a thick QCOW2 if you pick the wrong flags or convert through the wrong path.
- VMware snapshots are not backups. Snapshot chains can be fragile; conversions that ignore chain structure often produce disks that boot but are subtly inconsistent.
- QCOW2 performance improved dramatically over time. Modern QEMU has better caching and aio options, but QCOW2 metadata still costs IOPS, especially on random-write workloads.
- Proxmox historically leaned on raw for block storage. On ZFS and LVM-thin, raw volumes are usually the performance-and-sanity choice; QCOW2 shines on filesystem-backed storage.
Preflight checks: don’t convert what you don’t understand
Before you convert, you need to know what you have, where it lives, and what “correct” looks like. Most failed migrations are not conversion bugs. They’re missing context.
Task 1: Identify the VMDK type and backing files
cr0x@server:~$ ls -lah /mnt/incoming/vmware-export/
total 68G
drwxr-xr-x 2 root root 4.0K Dec 28 10:14 .
drwxr-xr-x 12 root root 4.0K Dec 28 10:10 ..
-rw------- 1 root root 512 Dec 28 10:12 disk.vmdk
-rw------- 1 root root 68G Dec 28 10:12 disk-flat.vmdk
What it means: This is the classic descriptor + flat extent. The small disk.vmdk is the descriptor; the big file is the data. If you only copied the descriptor, you copied the label, not the disk.
Decision: Keep both files together. Do not rename one without editing the descriptor. Treat them as a pair.
Task 2: Inspect the descriptor file (when it exists)
cr0x@server:~$ sed -n '1,120p' /mnt/incoming/vmware-export/disk.vmdk
# Disk DescriptorFile
version=1
encoding="UTF-8"
CID=fffffffe
parentCID=ffffffff
createType="vmfs"
# Extent description
RW 142606336 VMFS "disk-flat.vmdk"
# The Disk Data Base
ddb.virtualHWVersion = "13"
ddb.adapterType = "lsilogic"
ddb.geometry.cylinders = "8874"
ddb.geometry.heads = "255"
ddb.geometry.sectors = "63"
ddb.toolsInstallType = "4"
What it means: The extent line tells you size (in sectors) and the backing file. parentCID and CID hint about snapshot relationships. adapterType matters for boot after import (PVSCSI vs LSI vs SATA).
Decision: If parentCID doesn’t look like ffffffff and you have snapshot deltas, stop and locate the chain. Converting the wrong link is how you get “boots fine, data missing.”
Task 3: Ask qemu-img what it thinks the file is
cr0x@server:~$ qemu-img info /mnt/incoming/vmware-export/disk.vmdk
image: /mnt/incoming/vmware-export/disk.vmdk
file format: vmdk
virtual size: 68 GiB (72980889600 bytes)
disk size: 68 GiB
cluster_size: 65536
Format specific information:
cid: 4294967294
parent cid: 4294967295
create type: vmfs
extents:
[0]:
virtual size: 68 GiB
filename: /mnt/incoming/vmware-export/disk-flat.vmdk
format: FLAT
What it means: qemu-img sees it as VMDK and found the flat extent. Good. If it errors here, your conversion will also fail.
Decision: If extents is missing or filename points somewhere else, fix path issues first (often a moved/renamed flat file).
Task 4: Verify you’re not dealing with a snapshot chain by accident
cr0x@server:~$ find /mnt/incoming/vmware-export -maxdepth 1 -type f -name "*.vmdk" -printf "%f\n" | sort
disk-flat.vmdk
disk.vmdk
What it means: Only two files. Probably no snapshot chain in this export. In a chain you’d see files like -000001.vmdk and friends.
Decision: If you do see delta disks, identify the active “current” descriptor and ensure all parents are present before converting.
Task 5: Check filesystem free space where conversion will run
cr0x@server:~$ df -h /mnt/convert
Filesystem Size Used Avail Use% Mounted on
/dev/sdb1 1.8T 1.1T 650G 63% /mnt/convert
What it means: You have 650G free. Converting a 68G thin disk to a thick format could expand. Even QCOW2 can bloat during conversion depending on options and source layout.
Decision: Ensure worst-case space. If in doubt: convert to raw on block storage directly (LVM-thin/ZFS) instead of creating a huge intermediate file.
Task 6: Baseline the storage performance before you blame qemu-img
cr0x@server:~$ fio --name=readtest --filename=/mnt/convert/.fiotest --size=2G --direct=1 --rw=read --bs=1M --iodepth=16 --numjobs=1 --runtime=30 --time_based --group_reporting
readtest: (g=0): rw=read, bs=(R) 1024KiB-1024KiB, (W) 1024KiB-1024KiB, (T) 1024KiB-1024KiB, ioengine=psync, iodepth=16
fio-3.33
Starting 1 process
readtest: Laying out IO file (1 file / 2048MiB)
Jobs: 1 (f=1): [R(1)][100.0%][r=965MiB/s][r=965 IOPS][eta 00m:00s]
read: IOPS=952, BW=952MiB/s (998MB/s)(27.9GiB/30001msec)
clat (usec): min=402, max=10231, avg=1032.11, stdev=210.44
What it means: You’ve got ~950 MiB/s sequential read. If conversion is crawling at 40 MiB/s, the bottleneck is not “the disk is slow” in general—it’s somewhere else (write path, CPU, single-threaded behavior, compression, network).
Decision: Record baseline read/write. You’ll need it when someone says “Proxmox is slow.”
Joke #1: A VMDK snapshot chain is like a corporate org chart: it looks stable until you ask who actually owns the data.
Core conversion patterns with qemu-img (and why)
qemu-img is the workhorse for format conversions. The critical part is choosing:
- Output format: qcow2 or raw (and sometimes raw is the smarter choice).
- Allocation behavior: sparse vs preallocated.
- I/O characteristics: cache and aio settings that can speed conversion without lying about durability.
Task 7: Dry-run your understanding with info on both source and intended output
cr0x@server:~$ qemu-img info --output=json /mnt/incoming/vmware-export/disk.vmdk
{
"virtual-size": 72980889600,
"filename": "/mnt/incoming/vmware-export/disk.vmdk",
"format": "vmdk",
"actual-size": 72980889600,
"format-specific": {
"type": "vmfs",
"cid": 4294967294,
"parent-cid": 4294967295
}
}
What it means: JSON output is convenient for scripts and sanity checks. virtual-size matters; actual-size tells you how big the file currently is (for flat extents it may match virtual size).
Decision: Decide whether you’re keeping sparse behavior (thin) or going thick. For most migrations, keep it sparse unless you have a reason not to.
Task 8: Convert VMDK to QCOW2 (sane defaults)
cr0x@server:~$ qemu-img convert -p -f vmdk -O qcow2 /mnt/incoming/vmware-export/disk.vmdk /mnt/convert/disk.qcow2
(0.00/100%)
(12.34/100%)
(54.87/100%)
(100.00/100%)
What it means: -p shows progress. This is the baseline. No fancy caching flags. It will be correct, but may not be the fastest.
Decision: Use this if you’re doing a one-off or you can’t afford cleverness. “Boring and correct” is a valid operational strategy.
Task 9: Verify the result’s integrity (read-only check)
cr0x@server:~$ qemu-img check -r all /mnt/convert/disk.qcow2
No errors were found on the image.
443392/443392 = 100.00% allocated, 0.00% fragmented, 0.00% compressed clusters
Image end offset: 72985100288
What it means: qemu-img check validates QCOW2 structure. “No errors” is what you want. If you see refcount errors, that’s a red flag.
Decision: If check reports corruption, don’t import. Reconvert from source. If corruption persists, suspect source chain issues or storage errors.
Task 10: Inspect the QCOW2 metadata for what you actually got
cr0x@server:~$ qemu-img info /mnt/convert/disk.qcow2
image: /mnt/convert/disk.qcow2
file format: qcow2
virtual size: 68 GiB (72980889600 bytes)
disk size: 12.4 GiB
cluster_size: 65536
Format specific information:
compat: 1.1
lazy refcounts: false
refcount bits: 16
corrupt: false
What it means: Virtual size is 68 GiB, but disk size is 12.4 GiB: sparse/thin result. Great for saving space; not always great for sequential write performance if your workload will immediately fill the disk.
Decision: If the workload is a database that will allocate heavily on first boot, consider preallocation strategies or raw on block storage.
Task 11: Faster conversion using safer caching knobs
cr0x@server:~$ qemu-img convert -p -f vmdk -O qcow2 -t none -T none /mnt/incoming/vmware-export/disk.vmdk /mnt/convert/disk-fast.qcow2
(0.00/100%)
(33.21/100%)
(71.09/100%)
(100.00/100%)
What it means: -t is source cache mode; -T is destination cache mode. none typically avoids double-caching and can reduce overhead. It doesn’t magically make slow storage fast; it just reduces useless buffering.
Decision: Use this when you control the host and trust the storage stack. If you’re writing to flaky network storage, you may prefer safer caching at the cost of speed.
Task 12: Convert to raw (often better for Proxmox on ZFS/LVM-thin)
cr0x@server:~$ qemu-img convert -p -f vmdk -O raw -t none -T none /mnt/incoming/vmware-export/disk.vmdk /mnt/convert/disk.raw
(0.00/100%)
(25.00/100%)
(62.50/100%)
(100.00/100%)
What it means: Raw is simple. No metadata tables. Minimal overhead. On block-backed Proxmox storage, raw frequently wins on latency and predictability.
Decision: If your Proxmox target is ZFS zvol or LVM-thin, strongly consider importing as raw into those backends instead of keeping a QCOW2 file.
Task 13: Convert with explicit subformat when qemu-img guesses wrong
cr0x@server:~$ qemu-img convert -p -f vmdk -O qcow2 -o subformat=qcow2 /mnt/incoming/vmware-export/disk.vmdk /mnt/convert/disk.qcow2
qemu-img: warning: -o subformat=qcow2 is not supported for output format qcow2
What it means: This is a deliberate “don’t do that” example. qemu-img will tell you when you’re trying meaningless options. People copy-paste random flags from old blog posts; the tool is not sentimental.
Decision: If you don’t know what an option does, don’t use it in a migration window. Stick to flags you can explain under pressure.
Import into Proxmox the way Proxmox likes it
Conversion is only half the job. You still need to attach the disk correctly: right bus, correct boot order, proper storage type, and a guest OS that can find its root device.
Task 14: Inspect Proxmox storage types and pick the right target
cr0x@server:~$ pvesm status
Name Type Status Total Used Available %
local dir active 100.0G 22.3G 72.6G 22.30%
local-lvm lvmthin active 800.0G 312.0G 488.0G 39.00%
zfspool zfs active 1.7T 840.0G 860.0G 49.41%
What it means: dir storages like local can hold QCOW2 files. lvmthin and zfs typically want raw volumes (though Proxmox can store qcow2 on some backends; you’ll usually regret it on block storage).
Decision: If you’re importing to local-lvm or zfspool, plan for raw import or let Proxmox convert during import.
Task 15: Create a VM shell (no disk yet), then import the disk
cr0x@server:~$ qm create 120 --name vm-imported --memory 8192 --cores 4 --net0 virtio,bridge=vmbr0
created VM 120
What it means: You now have a VM config to attach the disk to.
Decision: Choose VirtIO for NIC unless guest OS lacks drivers. If it’s Windows without VirtIO drivers, plan driver injection or start with e1000 and migrate later.
Task 16: Import a QCOW2 file into Proxmox storage
cr0x@server:~$ qm importdisk 120 /mnt/convert/disk.qcow2 local-lvm
importing disk 'unused0:local-lvm:vm-120-disk-0'...
transferred 0.0 B of 68.0 GiB (0.00%)
transferred 10.2 GiB of 68.0 GiB (15.00%)
transferred 68.0 GiB of 68.0 GiB (100.00%)
Successfully imported disk as 'unused0:local-lvm:vm-120-disk-0'
What it means: Proxmox imported into local-lvm and likely converted to raw volume under the hood. The disk appears as unused0 until you attach it.
Decision: Attach it to the correct controller (VirtIO SCSI for modern Linux/Windows with drivers; SATA/IDE only for compatibility).
Task 17: Attach the imported disk and set boot order
cr0x@server:~$ qm set 120 --scsihw virtio-scsi-pci --scsi0 local-lvm:vm-120-disk-0 --boot order=scsi0
update VM 120: -scsihw virtio-scsi-pci -scsi0 local-lvm:vm-120-disk-0 -boot order=scsi0
What it means: The disk is now the boot device on a VirtIO SCSI controller, a generally good default for performance and features (discard, queueing).
Decision: If the guest won’t boot and previously used LSI Logic in VMware, try --scsihw lsi temporarily to get it up, then migrate to VirtIO later.
Task 18: Verify VM config, including machine type and BIOS/UEFI
cr0x@server:~$ qm config 120
boot: order=scsi0
cores: 4
memory: 8192
name: vm-imported
net0: virtio=BC:24:11:8A:19:FE,bridge=vmbr0
scsi0: local-lvm:vm-120-disk-0,iothread=1
scsihw: virtio-scsi-pci
What it means: Look for missing bios / efidisk0 if the original VM used UEFI. If it was UEFI on VMware and you boot with SeaBIOS here, expect sadness.
Decision: Match firmware: if UEFI, set --bios ovmf and add an EFI disk. If legacy, keep SeaBIOS.
Task 19: Convert on the fly with Proxmox if you want fewer steps
cr0x@server:~$ qm importdisk 120 /mnt/incoming/vmware-export/disk.vmdk zfspool
importing disk 'unused0:zfspool:vm-120-disk-0'...
transferred 0.0 B of 68.0 GiB (0.00%)
transferred 68.0 GiB of 68.0 GiB (100.00%)
Successfully imported disk as 'unused0:zfspool:vm-120-disk-0'
What it means: Proxmox can import directly from VMDK to the chosen storage, converting as needed. This reduces intermediate files and the chance you forget to clean up. It also concentrates risk on the Proxmox node doing the work.
Decision: If you do this, do it on a node with headroom and stable storage. Don’t run big conversions on the node already sweating under production load.
Performance tuning: speed without self-sabotage
Conversion speed is shaped by a few boring constraints: read bandwidth, write bandwidth, CPU (for checksums/metadata), and the source format’s layout. QCOW2 adds metadata overhead; VMDK snapshot chains add random reads. And network storage adds latency that turns sequential writes into a slow-motion documentary.
Know what you’re optimizing for
- Fast conversion time: minimize CPU overhead, avoid double caching, write sequentially to fast local storage, then move.
- Fast VM runtime: choose the right final format and backend; raw on block storage is hard to beat.
- Space efficiency: QCOW2 sparse is good; compression can help but adds CPU and can hurt latency later.
Task 20: Measure CPU as a bottleneck during conversion
cr0x@server:~$ pidstat -dru -p $(pgrep -n qemu-img) 1
Linux 6.8.12 (server) 12/28/2025 _x86_64_ (32 CPU)
11:04:12 UID PID %usr %system %CPU CPU kB_rd/s kB_wr/s Command
11:04:13 0 44122 85.00 10.00 95.00 7 980000.00 420000.00 qemu-img
What it means: If %CPU is pegged and I/O rates are below what the storage can do, you’re CPU-bound—often due to QCOW2 metadata handling or source format complexity.
Decision: If CPU-bound, consider converting to raw, or convert on a beefier node, or avoid compression/encryption features. Don’t expect cache flags to fix a CPU ceiling.
Task 21: Confirm I/O wait when conversion is “slow”
cr0x@server:~$ iostat -xm 1 3
Linux 6.8.12 (server) 12/28/2025 _x86_64_ (32 CPU)
avg-cpu: %user %nice %system %iowait %steal %idle
18.20 0.00 6.11 52.40 0.00 23.29
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz aqu-sz %util
nvme0n1 220.0 880000.0 0.0 0.00 1.10 4000.0 160.0 640000.0 0.0 0.00 2.80 4000.0 0.70 78.00
What it means: High %iowait plus high device util means your storage is the bottleneck. If util is low but iowait is high, suspect network storage or filesystem locks/latency.
Decision: Move conversion to faster local scratch storage and then import. Or, if the destination is the slow part, pick a better target backend (local-lvm/zfs on SSD) for the import step.
Task 22: Use a local scratch path to avoid writing QCOW2 over NFS
cr0x@server:~$ mount | grep -E 'nfs|cifs'
nas01:/exports/vm-migrations on /mnt/incoming type nfs4 (rw,relatime,vers=4.2,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2)
What it means: Source is on NFS. Reading is often okay; writing the output QCOW2 back to NFS during conversion is where performance goes to retire early.
Decision: Copy source locally first, convert locally, then import. Yes, it’s extra steps. No, you won’t regret it.
Task 23: Copy the VMDK locally while preserving sparseness (when applicable)
cr0x@server:~$ rsync -aH --sparse --info=progress2 /mnt/incoming/vmware-export/ /mnt/local-scratch/vmware-export/
68,000,000,000 100% 720.00MB/s 0:01:30 (xfr#2, to-chk=0/3)
What it means: --sparse can preserve holes on filesystems that support them, which matters when you’re staging thin images.
Decision: If the source is truly flat/thick, sparse doesn’t help. But it doesn’t hurt much either; keep it in your standard migration muscle memory.
Task 24: Preallocate output when you care about steady write performance later
cr0x@server:~$ qemu-img convert -p -f vmdk -O qcow2 -o preallocation=metadata /mnt/local-scratch/vmware-export/disk.vmdk /mnt/convert/disk-prealloc.qcow2
(0.00/100%)
(49.00/100%)
(100.00/100%)
What it means: preallocation=metadata allocates QCOW2 metadata upfront, reducing some allocation churn later without fully preallocating all data clusters.
Decision: Use this when the guest will do lots of random writes early (databases, busy application servers). If storage space is tight, avoid full preallocation.
Task 25: Verify you didn’t accidentally create a giant thick QCOW2
cr0x@server:~$ du -h /mnt/convert/disk-prealloc.qcow2
13G /mnt/convert/disk-prealloc.qcow2
What it means: Still relatively small: good. If it equals virtual size, you made it thick.
Decision: If you unintentionally thickened, re-run conversion with correct options or consider raw on block storage instead.
Task 26: Confirm discard/TRIM expectations (for thin backends)
cr0x@server:~$ qm set 120 --scsi0 local-lvm:vm-120-disk-0,discard=on
update VM 120: -scsi0 local-lvm:vm-120-disk-0,discard=on
What it means: Enables guest discard (where supported). On thin-provisioned backends, this can return freed blocks. On some stacks it does nothing; on others it helps keep the pool healthy.
Decision: Turn it on for Linux guests with fstrim and modern kernels. For some Windows setups, validate behavior before you assume it’s working.
Joke #2: Turning on every “performance” flag at once is like adding eight spoilers to a sedan—technically modified, still slow, now louder.
Fast diagnosis playbook (find the bottleneck in minutes)
This is the order I use when conversion or first boot performance looks wrong. It avoids rabbit holes and gets you to a concrete “it’s CPU / it’s storage / it’s network / it’s guest config” answer fast.
First: prove the source image is coherent
- qemu-img info on the VMDK. If it fails, stop; fix descriptor/extent/snapshots.
- List the directory for missing delta files or renamed extents.
- If snapshot chain exists, identify active descriptor (the one the VM used) and ensure all parents exist.
Second: locate the throughput limit
- Baseline destination write speed with
fioor at least watchiostat -xm. - Check if conversion is CPU-bound with
pidstat. - If source is network storage, copy locally and retry conversion to separate network from conversion overhead.
Third: validate the output and the Proxmox attachment
- qemu-img check on the QCOW2 (or verify raw size and checksums if you can).
- qm config: correct controller, boot order, BIOS/UEFI match.
- Guest drivers: VirtIO storage/NIC drivers present? If not, expect boot failures or missing NICs.
Task 27: Confirm whether you’re CPU-bound or I/O-bound in one glance
cr0x@server:~$ top -b -n 1 | head -n 15
top - 11:06:01 up 10 days, 2:14, 2 users, load average: 9.12, 8.44, 7.90
Tasks: 412 total, 2 running, 410 sleeping, 0 stopped, 0 zombie
%Cpu(s): 61.3 us, 7.2 sy, 0.0 ni, 8.4 id, 23.1 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 128000.0 total, 24000.0 free, 12000.0 used, 92000.0 buff/cache
MiB Swap: 16000.0 total, 15950.0 free, 50.0 used. 108000.0 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
44122 root 20 0 221668 29384 6120 R 298.0 0.0 1:22.19 qemu-img
What it means: High CPU (qemu-img using multiple cores) plus high wa (I/O wait) means you’re bottlenecked on both; throwing flags at qemu-img won’t fix the slow disk.
Decision: Move the job to faster storage or schedule it when storage is quiet. If that’s not possible, convert to raw and let the backend handle allocation.
Common mistakes: symptoms → root cause → fix
This section is written in the language of on-call: what you see, what it usually means, and what actually works.
1) qemu-img: “Could not open ‘…’: invalid VMDK descriptor”
Symptoms: qemu-img info or convert fails immediately with descriptor parsing errors.
Root cause: The descriptor file is corrupted, edited incorrectly, newline/encoding issues, or it references a missing/renamed extent file.
Fix:
- Open the descriptor and verify the extent filename matches what’s on disk.
- Ensure the flat extent exists and is readable.
- If descriptor is gone but you have
*-flat.vmdk, reconstruct a descriptor (carefully) or re-export from VMware.
2) Conversion “succeeds” but the VM boots to initramfs / cannot find root device
Symptoms: Linux drops to emergency shell; root device mismatch after import.
Root cause: Disk controller changed (e.g., VMware LSI Logic to Proxmox VirtIO SCSI) and initramfs lacks drivers, or fstab uses device names that changed (sda vs vda).
Fix:
- Temporarily switch controller to a more compatible one (e.g., LSI) to boot, then install VirtIO drivers and rebuild initramfs.
- Use UUIDs in fstab and bootloader config if possible.
3) Windows blue-screens after import (INACCESSIBLE_BOOT_DEVICE)
Symptoms: BSOD on boot right after Proxmox import.
Root cause: Windows lacks the storage controller driver for the new virtual controller (VirtIO SCSI), or boot mode mismatch (UEFI vs BIOS).
Fix:
- Boot with a controller Windows already supports (SATA, LSI) first.
- Install VirtIO drivers inside Windows, then switch controller.
- Match firmware mode and ensure correct boot order.
4) Conversion is painfully slow on NFS/CIFS
Symptoms: Conversion runs at tens of MB/s despite fast disks locally.
Root cause: Writing output over network storage with high latency; QCOW2 metadata updates amplify small writes.
Fix: Stage locally (NVMe scratch), convert locally, then import to the final backend. If you must write over network, prefer raw and bigger I/O, and accept it may still be slow.
5) Imported VM disk is “unused0” and VM doesn’t boot
Symptoms: Disk exists in Proxmox storage but not attached as a boot device.
Root cause: qm importdisk imports but does not attach; boot order not set.
Fix: Attach disk with qm set and set --boot order=....
6) Disk size looks wrong after conversion
Symptoms: QCOW2 file size equals virtual size when you expected thin, or vice versa.
Root cause: Preallocation options, source was thick flat extent, or conversion path expanded data (zeroed blocks written as allocated).
Fix: Reconvert with desired options; for thin targets ensure sparse-friendly copy methods and avoid tools that de-sparsify (some naive copies do).
7) “Failed to get write lock” / “image is in use” during conversion
Symptoms: qemu-img refuses to write or Proxmox can’t start VM.
Root cause: The disk is opened by another process (backup job, another conversion, a VM still referencing it).
Fix: Identify the process holding the file/volume and stop it; avoid converting in place on an active datastore.
Task 28: Find who has a disk file open (classic “why is it locked?”)
cr0x@server:~$ lsof /mnt/convert/disk.qcow2 | head
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
qemu-img 44122 root 3u REG 8,17 13314398624 131073 /mnt/convert/disk.qcow2
What it means: The file is actively open by qemu-img. If you see qemu-system-x86_64, a VM is using it.
Decision: Don’t fight locks. Stop the consumer cleanly, then proceed.
Three corporate mini-stories (because the disk doesn’t care about your feelings)
Mini-story 1: The incident caused by a wrong assumption
The team had a VMware export: a neat little server.vmdk file sitting in a shared folder. Someone copied just that file to the Proxmox node and kicked off qemu-img convert. qemu-img complained about missing extents. They shrugged and re-exported, got the same thing, then decided the tool was “finicky.”
The wrong assumption was simple: they thought a VMDK is always self-contained. In their environment, ESXi stored a descriptor file plus a massive flat file, and their export workflow preserved both—but only the descriptor looked “important.” The big -flat.vmdk sat next to it like a silent accomplice.
They finally copied the flat extent too, and conversion worked. But then the VM booted with filesystem errors. The real damage happened earlier: during the first attempt, they tried to “fix” the descriptor by editing the extent size and filename based on guesswork, then converted from a mismatched descriptor pointing at a different flat file from an older export. The conversion was accurate—accurate to the wrong source.
The fix was not heroic. They restarted from the original export, verified the descriptor’s extent reference, verified file checksums on both files, and only then ran conversion. The downtime was longer than it needed to be, and the lesson was embarrassingly cheap: never edit image metadata unless you can prove what you’re doing.
Mini-story 2: The optimization that backfired
A different group ran nightly migrations for a lab refresh. They wanted speed. They found a handful of flags online and built a “turbo convert” wrapper: aggressive caching modes, parallel jobs, and writing directly to a network share to “avoid extra copying.”
It was fast—until it wasn’t. Some conversions produced QCOW2 images that passed a superficial qemu-img info but later failed under load with corruption warnings. Others booted fine but had subtle application errors that looked like software bugs. The migrations became a support nightmare: every imported VM was suspect.
The backfire was mostly about durability semantics. They mixed caching options with network storage behavior and treated “conversion finished” as “bytes safely landed.” Network hiccups and server-side caching meant the job could complete while the storage hadn’t truly committed everything the team assumed it had.
They rolled back the wrapper. The new process looked slower on paper but finished faster end-to-end because it stopped producing broken artifacts: stage locally, convert locally with conservative caching, validate with qemu-img check, then import. The best performance optimization was eliminating rework.
Mini-story 3: The boring but correct practice that saved the day
A finance-adjacent system was migrating from VMware to Proxmox under a tight window. The team did something profoundly unfashionable: they wrote a checklist and followed it. Not a “nice to have” checklist. A gatekeeping checklist that refused to proceed until each step produced expected output.
They captured qemu-img info JSON for every source, stored it with the migration ticket, and recorded the Proxmox VM config after import. They also ran qemu-img check on every QCOW2 artifact and kept the log. It was tedious, the way good operations usually are.
During cutover, one VM failed to boot. No panic. They compared the original VMware adapter type in the descriptor (lsilogic) to the Proxmox config (VirtIO SCSI). They switched the controller to LSI, booted, installed VirtIO drivers, switched back, and moved on. The checklist didn’t prevent the issue, but it prevented thrash.
What saved the day was not a clever command. It was disciplined observability: every step left behind evidence. When something broke, they didn’t guess—they compared.
Checklists / step-by-step plan
Use one of these depending on your storage backend and appetite for intermediate files.
Plan A: Safe and portable (VMDK → QCOW2 file → import)
- Inventory files: ensure you have descriptor + flat extent (or a single monolithic VMDK).
- Run
qemu-img infoon source; confirm virtual size and extents. - Check free space on conversion filesystem (worst-case planning).
- Stage locally if source/destination is network storage.
- Convert with conservative flags:
qemu-img convert -p -f vmdk -O qcow2. - Run
qemu-img check -r allon QCOW2. - Create VM shell in Proxmox:
qm create. - Import disk:
qm importdiskto the chosen storage. - Attach disk and set boot order:
qm set. - First boot validation: confirm OS sees disk, NIC works, time sync, and services start.
Plan B: Proxmox-centric (import directly from VMDK)
- Verify source VMDK with
qemu-img info. - Create VM shell with correct firmware expectation (UEFI vs BIOS as best you know).
- Run
qm importdisk VMID disk.vmdk STORAGE. - Attach disk with correct controller and boot order.
- Boot, validate, then adjust performance options (iothread, discard, cache) carefully.
Plan B is fine for routine migrations when you trust your Proxmox node health and want fewer artifacts to manage. Plan A is better when you need a portable QCOW2 and a repeatable audit trail.
Task 29: Capture a reproducible “conversion record” (cheap audit trail)
cr0x@server:~$ qemu-img info --output=json /mnt/incoming/vmware-export/disk.vmdk | tee /mnt/convert/disk.vmdk.info.json
{
"virtual-size": 72980889600,
"filename": "/mnt/incoming/vmware-export/disk.vmdk",
"format": "vmdk",
"actual-size": 72980889600,
"format-specific": {
"type": "vmfs",
"cid": 4294967294,
"parent-cid": 4294967295
}
}
What it means: You’ve saved a machine-readable record of what you converted. This is how you avoid “what did we import again?” two weeks later.
Decision: Keep these records with change tickets. It’s boring. It works.
Task 30: Confirm the imported disk exists on the target storage
cr0x@server:~$ pvesm list local-lvm | grep vm-120-disk-0
local-lvm:vm-120-disk-0 vm-120-disk-0 68.00G
What it means: The volume is present and sized correctly.
Decision: If size is wrong, stop and investigate before boot. A wrong size can indicate you imported the wrong file or truncated data.
Task 31: Start the VM and watch early boot from the host side
cr0x@server:~$ qm start 120
started VM 120
What it means: VM launched. Now you need console visibility.
Decision: If it fails instantly, check Proxmox task logs and storage errors. If it starts but doesn’t boot, check firmware/controller mismatch.
One quote that belongs in every migration runbook
Hope is not a strategy.
— General Gordon R. Sullivan
FAQ
1) Should I convert to QCOW2 or raw for Proxmox?
If your target storage is dir/NFS and you want a portable file, QCOW2 is fine. If your target is LVM-thin or ZFS, raw is usually faster and simpler. If you insist on QCOW2 on block storage, you’re paying overhead for features you likely won’t use.
2) Can Proxmox import a VMDK directly?
Yes, qm importdisk can import from VMDK into a Proxmox storage target, converting as needed. It’s convenient, but it doesn’t remove the need to validate the source and attach the disk correctly.
3) Why does qemu-img say “invalid VMDK descriptor”?
Most often the descriptor references a flat extent file that’s missing, renamed, or in a different path. Sometimes the descriptor is corrupted or edited incorrectly. Validate the extent filename and ensure the flat file is present.
4) My converted disk boots but data is missing. How?
You likely converted the wrong layer of a snapshot chain (a parent instead of the active child), or you didn’t include all delta files. VMware snapshot chains must be complete and consistent for conversion to represent the current state.
5) Why is conversion slower than copying the file?
Conversion may involve metadata updates (QCOW2), random reads (snapshot chains), or network latency amplification (writing QCOW2 to NFS). Copying is often sequential and simpler.
6) Do I need to worry about UEFI vs BIOS during import?
Yes. If the VM was UEFI on VMware and you boot it with legacy BIOS on Proxmox, you can get a non-booting system that looks like “disk problems” but isn’t. Match firmware early.
7) What’s the safest verification step after conversion?
Run qemu-img check -r all for QCOW2 structural integrity, and then do a test boot in an isolated network. Integrity checks won’t catch all filesystem-level inconsistencies, but they catch image-level corruption.
8) Can I speed conversion by using compression in QCOW2?
You can, but it’s a trade. Compression costs CPU and can hurt random I/O latency later. For production VMs, I’d rather buy storage than buy mysterious latency.
9) My Windows VM won’t boot with VirtIO. What’s the least painful path?
Boot with a controller Windows supports out of the box (often SATA/LSI), then install VirtIO drivers, then switch to VirtIO SCSI. Do it in controlled steps, not all at once.
10) Should I convert on the Proxmox node or elsewhere?
Convert where you have stable I/O and headroom. If Proxmox nodes are busy, use a dedicated conversion host and then import. The best migration is the one that doesn’t compete with production workloads.
Practical next steps
Do the migration like you’d do a storage change in production: identify what you have, pick the correct target format for the backend, run conversion with flags you understand, and validate before boot. Avoid “speed hacks” until you’ve proven where the bottleneck is.
Next steps that pay off immediately:
- Run
qemu-img infoon every VMDK you plan to migrate and save the JSON output with the ticket. - Pick your final storage target first (dir vs LVM-thin vs ZFS), then decide QCOW2 vs raw accordingly.
- Adopt the fast diagnosis playbook: source coherence → throughput limit → Proxmox attachment.
- Standardize on one conversion pattern per backend and stop improvising during change windows.