The default images pool that ships with libvirt is a directory pool, which means most people run one for years before they ever configure one on purpose. That familiarity hides a few behaviours worth knowing before you build a second one: a directory pool reports the size of whatever filesystem it happens to sit on rather than a size of its own, it will happily go stale if you copy files in behind its back, and it is the only pool type where a 20 GiB disk can cost 196 KiB.
This guide sets up a KVM directory storage pool with virsh, then works through the parts the reference documentation leaves out: what pool-build actually creates, how to adopt disk images that already exist on disk, how backing files turn one base image into many guests, and which teardown command destroys data versus which one only forgets about it. Everything below was run on Ubuntu 26.04 with libvirt 12.0.0 and QEMU 10.2.1 in August 2026.
Prerequisites
A KVM host with libvirt running is all you need. There is no device to dedicate and nothing to format, which is the main argument for this pool type. The KVM and QEMU install guide covers the packages if the hypervisor is not up yet.
Pick the target directory with some care. A pool under /var/lib/libvirt inherits sensible ownership and stays out of the way of backups that target user data. A pool under a user home directory works until QEMU drops privileges and cannot traverse a 750 home, at which point guests fail to start. Space planning is just filesystem planning here: the pool grows into whatever the underlying filesystem has free, so give /var its own volume on any host that will hold more than a couple of guests.
Set the pool variables
The pool name and its target path repeat throughout. Export both so the commands below paste without editing:
export POOL_NAME="vmstore"
export POOL_PATH="/var/lib/libvirt/vmstore"
They last for the current shell only, so re-export after reconnecting. Print them once before going further:
echo "Pool: ${POOL_NAME} -> ${POOL_PATH}"
1. Confirm support and the volume formats on offer
The dir backend is present in every mainstream libvirt build, but the same query also prints the image formats your build will accept:
sudo virsh pool-capabilities | grep -o "<pool type='dir' supported='yes'>"
One line back means the type is available:
<pool type='dir' supported='yes'>
The volume formats accepted on Ubuntu 26.04 are raw, qcow, qcow2, qed, vmdk, vpc, vdi, vhd, bochs, cloop, cow, dmg, iso, fat and ploop. That list is the practical difference between this pool type and the block-based ones. Anything qemu-img understands can live here, including formats you are migrating away from rather than towards.
2. Define and build the pool
A directory pool needs one argument. No source device, no format, no partition table:
sudo virsh pool-define-as "${POOL_NAME}" dir --target "${POOL_PATH}"
sudo virsh pool-build "${POOL_NAME}"
sudo virsh pool-start "${POOL_NAME}"
sudo virsh pool-autostart "${POOL_NAME}"
On this pool type pool-build means mkdir and nothing else. Skip it when the directory already exists. Skip it when the directory does not exist and pool-start refuses to run, which is the one error worth recognising here.
Error: “cannot open directory ‘/var/lib/libvirt/vmstore’: No such file or directory”
libvirt lets you define a pool pointing anywhere, valid or not, because defining is only writing XML into /etc/libvirt/storage/. The path is checked at start time:
error: Failed to start pool vmstore
error: cannot open directory '/var/lib/libvirt/vmstore': No such file or directory
Run virsh pool-build vmstore and start it again. Creating the directory yourself with mkdir -p works identically. This is also the reason the Cockpit and virt-manager wizards insist the directory exists before they will activate a pool: neither calls build for you.
Here is the whole sequence including the ownership that build leaves behind:

Note the mode on that directory. libvirt builds it 0711 owned by root, not 0755, so an unprivileged user can traverse into a known path but cannot list what is inside. Shell globs like ls ~/pool/*.qcow2 fail for a normal user while qemu-img info on the full path succeeds. Change it deliberately with a permissions block in the pool XML if a non-root user needs to browse the directory.
3. What the capacity numbers actually mean
Check the pool and the figures look wrong at first glance:
sudo virsh pool-info "${POOL_NAME}"
A pool created seconds ago already reports 14 GiB allocated:
Name: vmstore
UUID: cde008ff-bd12-4c6e-b06a-374c640e7cb3
State: running
Persistent: yes
Autostart: yes
Capacity: 192.85 GiB
Allocation: 14.11 GiB
Available: 178.74 GiB
Those three numbers are statvfs on the host filesystem, not a property of the pool. Capacity is the size of the filesystem holding the directory, and allocation is everything used on it, including the operating system and every unrelated file. A directory pool has no boundary of its own, so two pools on the same filesystem report identical numbers and quietly compete for the same free space.
That is the practical trade against a filesystem pool, which mounts a dedicated device and therefore reports a real ceiling you can alert on. If you need per-pool capacity limits, a directory pool is not the tool. If you need a place to put disk images in the next thirty seconds, nothing else comes close.
4. Create volumes
Volumes are files, so names and formats are yours to choose:
sudo virsh vol-create-as "${POOL_NAME}" web10-data.qcow2 10G --format qcow2
The qcow2 format allocates lazily, so a 10 GiB volume starts at a couple of hundred kilobytes and grows only as the guest writes. Raw volumes take their full size immediately unless the filesystem supports sparse files, and even then tools report them differently. Pick qcow2 unless you have measured a reason not to.
5. Adopt images that are already on disk
This is the behaviour that makes directory pools worth understanding. Copy an image into the target path with ordinary tools:
sudo cp ubuntu-26.04-server-cloudimg-amd64.img "${POOL_PATH}/ubuntu-2604-base.qcow2"
sudo virsh vol-list "${POOL_NAME}"
The file is on disk and libvirt does not know it exists:
Name Path
--------------
The pool keeps a cached index that is rebuilt on demand rather than watched live. One command reconciles it:
sudo virsh pool-refresh "${POOL_NAME}"
sudo virsh vol-list "${POOL_NAME}" --details
The image is now a first class volume, with its real qcow2 geometry read from the file:
Name Path Type Capacity Allocation
----------------------------------------------------------------------------------------------------------
ubuntu-2604-base.qcow2 /var/lib/libvirt/vmstore/ubuntu-2604-base.qcow2 file 3.50 GiB 820.59 MiB
Migrating an existing image collection into libvirt management is therefore a copy and a refresh, with no import step and no metadata to convert. The same trick works in reverse for backups: rsync the directory somewhere, and a refresh on the far side gives you the same volumes.
Error: “Storage volume not found: no storage vol with matching path”
The index cuts both ways. Delete an image with rm and vol-list keeps listing it, because nothing told the pool. Any command that touches the file then fails:
error: Storage volume not found: no storage vol with matching path '/var/lib/libvirt/vmstore/web10.qcow2'
Nothing is broken. Run virsh pool-refresh vmstore and the phantom entry disappears. The habit that avoids the confusion entirely is to use virsh vol-delete rather than rm, since that removes the file and updates the index in one step.
6. Clone a base image with a backing file
Backing files are the payoff for keeping images as files. A new volume can reference an existing one as a read only base and store only the blocks that differ:
sudo virsh vol-create-as "${POOL_NAME}" web10.qcow2 20G \
--format qcow2 \
--backing-vol ubuntu-2604-base.qcow2 \
--backing-vol-format qcow2
The result is a 20 GiB disk that occupies 196 KiB, pointing at an 820 MiB base:

Ten guests off one base image cost the base plus ten sets of deltas. Attach the clone to a running guest and the chain is transparent to it:
sudo virsh attach-disk web02 "${POOL_PATH}/web10.qcow2" vdc \
--driver qemu --subdriver qcow2 --targetbus virtio --persistent
Inside the guest the cloud image partition table is fully readable through a volume that has never had a byte written to it:
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
vdc 253:32 0 20G 0 disk
├─vdc1 253:33 0 2.4G 0 part
├─vdc13 253:45 0 1023M 0 part
├─vdc14 253:46 0 4M 0 part
└─vdc15 253:47 0 106M 0 part
Mounting /dev/vdc1 in the guest lists a complete Ubuntu root filesystem, and after all that reading the clone had grown to 584 KiB. Two rules keep this arrangement healthy. Never write to a base image that has children, because every child inherits the corruption. And never move or rename a base image, since the backing path is stored inside each child and a broken link takes the guest down with it. When a clone needs to become independent, virsh blockpull flattens it into a standalone image while the guest keeps running.
This is also the mechanism behind fast guest provisioning. Pair it with the cloud-init and virt-install workflow and a new VM is a seed ISO plus a few hundred kilobytes of qcow2 delta.
Removing a pool without deleting the images
Three commands sound alike and do very different things, which is why people hesitate to clean up pools at all. Tested on a scratch pool holding one volume:
| Command | Effect on the pool | Effect on the files |
|---|---|---|
virsh pool-destroy | Stops it. Definition stays. | Untouched |
virsh pool-undefine | Removes the definition entirely | Untouched |
virsh pool-delete | Attempts to remove the target directory | Refuses while the directory has contents |
The reassuring part is what pool-delete does when the directory is not empty:
error: Failed to delete pool scratchpool
error: failed to remove pool '/var/lib/libvirt/scratchpool': Directory not empty
On a directory pool the delete operation is an rmdir, so it cannot take your disk images with it. That safety net does not extend to every pool type. On a disk pool the equivalent build step rewrites the partition table on the device, and an LVM pool removes logical volumes for real. Read the pool type before assuming the same command means the same thing.
So the sequence to retire a directory pool while keeping its contents is stop, then undefine, then move the files wherever they belong. Point a new pool at that path later, run a refresh, and every volume comes back exactly as it was. The virsh reference has the rest of the pool subcommands if you are scripting this into a host rebuild.