Skip to content

Instantly share code, notes, and snippets.

@drewdomi
Created March 11, 2026 23:40
Show Gist options
  • Select an option

  • Save drewdomi/e9d5117654b6f8713779328e6a390667 to your computer and use it in GitHub Desktop.

Select an option

Save drewdomi/e9d5117654b6f8713779328e6a390667 to your computer and use it in GitHub Desktop.

Shrinking a QCOW2 Image with LUKS Encryption

Problem

Running qemu-img convert -O qcow2 -c alone does not reduce image size when the guest uses LUKS encryption. LUKS encrypts all blocks — including free space — making them appear allocated to the host. TRIM signals must pass through the full stack: guest → LUKS → virtio-disk → qcow2.


Environment

  • Host: Linux with libvirt (qemu:///system)
  • Guest: Linux with LUKS-encrypted root filesystem
  • Image format: qcow2

Stack Overview

Guest fstrim
    ↓
LUKS (allow_discards)
    ↓
virtio-disk (discard=unmap via libvirt XML)
    ↓
qcow2 (cluster deallocation)
    ↓
qemu-img convert (skips unallocated clusters)

Step 1 — Enable Discard in LUKS (Guest)

Edit /etc/crypttab and add the discard option to your LUKS entry:

# /etc/crypttab
# <name>        <device>          <keyfile>  <options>
luks-<uuid>     UUID=<your-uuid>  none       luks,discard

Edit /etc/fstab and add discard to the mount options:

# /etc/fstab
/dev/mapper/luks-<uuid>  /  ext4  defaults,discard  0 1

Rebuild initramfs and reboot:

sudo update-initramfs -u -k all
sudo reboot

Verify discard is active after reboot:

sudo dmsetup table | grep allow_discards
# Must show 'allow_discards' in output

Step 2 — Enable Discard at the Hypervisor Level (Host)

Why: Without this, TRIM signals from the guest are dropped before reaching the qcow2 file.

Find the correct libvirt URI (system vs session):

virsh -c qemu:///system list --all

Edit the VM XML:

virsh -c qemu:///system edit <vm-name>

Find the <driver> line inside the <disk> block and add discard="unmap":

<driver name="qemu" type="qcow2" discard="unmap"/>

Security note: Enabling discard on LUKS leaks metadata about which blocks are unused. File content remains encrypted. Acceptable tradeoff for local VMs; avoid on shared or untrusted storage.


Step 3 — Run fstrim Inside the Guest

Start the VM and run:

sudo fstrim -av
# Output must report actual bytes discarded

Shutdown cleanly:

sudo poweroff

Step 4 — Convert the Image (Host)

qemu-img convert -O qcow2 -c -p mxlinux.qcow2 mxlinux_new.qcow2
  • -O qcow2 — output format
  • -c — apply compression
  • -p — show progress

Validation

du -sh mxlinux_new.qcow2
qemu-img info mxlinux_new.qcow2 | grep -E "disk size|virtual size"
# disk size must reflect actual guest usage
# virtual size stays unchanged (expected)

Permanent libvirt URI Fix (Optional)

Add to your shell config (~/.bashrc or ~/.zshrc):

export LIBVIRT_DEFAULT_URI="qemu:///system"

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment