Two commands into the documented recipe, the build step fails with mkfs: failed to execute mkfs.auto: No such file or directory. The default source format for a KVM filesystem storage pool is auto, which works fine for mounting a device that already carries a filesystem but gives libvirt nothing to call when it has to create one. Almost every walkthrough of this pool type skips that, because almost every walkthrough formats the device by hand first and never asks libvirt to do it.
This guide covers the filesystem pool type end to end with virsh: confirming hypervisor support, preparing the backing partition, defining the pool with a format libvirt can actually use, creating thin qcow2 volumes, attaching one to a running guest, and checking what comes back after the host reboots. Every command and every block of output below was run on Ubuntu 26.04 with libvirt 12.0.0 and QEMU 10.2.1 in August 2026.
Prerequisites
You need a working KVM host with libvirt running. If the hypervisor is not up yet, the KVM and QEMU install steps cover the packages and the group membership.
You also need a block device or partition you are willing to reformat. A filesystem pool consumes the whole device it is given, so nothing on it survives the build step. Size it against what the guests will actually hold: a couple of small utility VMs fit in 20 GB, while anything holding database volumes or snapshots wants 200 GB and up. The lab here uses a spare 10 GB virtual disk at /dev/vdc, which is enough to demonstrate the mechanics but well under what a real host would allocate.
Set the pool variables
The pool name, the backing device, and the mount target each appear in a dozen commands below. Export them once so the rest of the guide pastes cleanly:
export POOL_NAME="guest_fs"
export POOL_DEV="/dev/vdc1"
export POOL_TARGET="/var/lib/libvirt/guest_fs"
export POOL_FS="ext4"
These live only in the current shell. Reconnecting over SSH or jumping into sudo -i drops them, so re-export before continuing a session. Confirm they are set before anything destructive runs:
echo "Pool: ${POOL_NAME}"
echo "Device: ${POOL_DEV}"
echo "Target: ${POOL_TARGET} (${POOL_FS})"
Confirm libvirt supports filesystem pools
The backend is compiled in on most distributions, but checking costs nothing and tells you the exact format list your build accepts:
sudo virsh pool-capabilities | grep -o "<pool type='fs' supported='yes'>"
A single matching line means the type is available:
<pool type='fs' supported='yes'>
The filesystem formats this backend accepts on Ubuntu 26.04 are auto, ext2, ext3, ext4, ufs, iso9660, udf, gfs, gfs2, vfat, hfs+, xfs, ocfs2 and vmfs. That list matters in a moment, because picking auto is what breaks the build.
Prepare the backing partition
libvirt will mount whatever device you name, so hand it a partition rather than the raw disk. A bare disk works, but a partition table makes the intent obvious to whoever inherits the host and leaves room to carve a second slice later.
sudo wipefs -a /dev/vdc
sudo parted -s /dev/vdc mklabel gpt
sudo parted -s /dev/vdc mkpart primary ext4 1MiB 100%
sudo partprobe /dev/vdc
The partition should now show up under the parent disk:
lsblk /dev/vdc
One 10 GB partition, no mount point yet:
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
vdc 253:32 0 10G 0 disk
└─vdc1 253:33 0 10G 0 part
Note the mkpart argument ext4 only writes a partition type flag. No filesystem exists on /dev/vdc1 at this point, which is exactly the state that trips the next step.
Define the pool with an explicit filesystem format
The one-liner form of pool-define-as takes the device and the mount target. Add --source-format and the whole thing works on the first pass:
sudo virsh pool-define-as "${POOL_NAME}" fs \
--source-dev "${POOL_DEV}" \
--source-format "${POOL_FS}" \
--target "${POOL_TARGET}"
libvirt writes the definition to /etc/libvirt/storage/ and reports it:
Pool guest_fs defined
Dump the generated XML to see what that flag actually produced:
sudo virsh pool-dumpxml "${POOL_NAME}"
The format element under source is the piece that decides whether libvirt can format the device later:
<pool type='fs'>
<name>guest_fs</name>
<uuid>c128ddf1-e126-4242-9d1f-419485f19b94</uuid>
<capacity unit='bytes'>0</capacity>
<allocation unit='bytes'>0</allocation>
<available unit='bytes'>0</available>
<source>
<device path='/dev/vdc1'/>
<format type='ext4'/>
</source>
<target>
<path>/var/lib/libvirt/guest_fs</path>
</target>
</pool>
Prefer to keep the definition in version control? Write the same XML to a file and define from it. Open the file:
sudo vim /etc/libvirt/pools/guest_fs.xml
Only four elements are mandatory, so the hand written version is short:
<pool type='fs'>
<name>guest_fs</name>
<source>
<device path='/dev/vdc1'/>
<format type='ext4'/>
</source>
<target>
<path>/var/lib/libvirt/guest_fs</path>
</target>
</pool>
Then load it:
sudo virsh pool-define /etc/libvirt/pools/guest_fs.xml
Error: “mkfs: failed to execute mkfs.auto: No such file or directory”
Leaving --source-format off the define command sets the format to auto, and the failure only surfaces later, at build time:
error: Failed to build pool guest_fs
error: internal error: Child process (mkfs -t auto /dev/vdc1) unexpected exit status 1: mkfs: failed to execute mkfs.auto: No such file or directory
auto is a mount time instruction, not a mkfs one. It tells the kernel to probe the superblock and figure out the type on its own, which is useful when the device is already formatted and useless when it is blank. libvirt passes the value straight through to mkfs -t, and there is no mkfs.auto helper on any distribution. Fix it by undefining and redefining with a real filesystem name:
sudo virsh pool-undefine "${POOL_NAME}"
sudo virsh pool-define-as "${POOL_NAME}" fs \
--source-dev "${POOL_DEV}" \
--source-format ext4 \
--target "${POOL_TARGET}"
Formatting the partition yourself with mkfs.ext4 before defining the pool also works, and auto is a reasonable choice in that case. The point is that one of the two has to know the filesystem type.
Error: “wrong fs type, bad option, bad superblock on /dev/vdc1”
This is the same root cause seen from the other end. pool-build without --overwrite succeeds, prints Pool guest_fs built, and creates nothing but the target directory. Starting the pool then fails when the mount finds no filesystem:
error: Failed to start pool guest_fs
error: internal error: Child process (mount -o nodev,nosuid,noexec -t auto /dev/vdc1 /var/lib/libvirt/guest_fs) unexpected exit status 32: mount: /var/lib/libvirt/guest_fs: wrong fs type, bad option, bad superblock on /dev/vdc1, missing codepage or helper program, or other error.
A plain pool-build on a filesystem pool is a directory creation, nothing more. That success message is honest about what it did and misleading about what you probably wanted. Only --overwrite reaches for mkfs. Confirm with blkid, which prints a partition UUID but no TYPE field while the device is still blank.
That mount line is also worth reading closely for another reason. libvirt hard codes nodev,nosuid,noexec on every filesystem pool mount, which is the right default for a directory full of disk images and a surprise if you were planning to keep helper scripts alongside them.
Build, start, and enable the pool
With a real format in the definition, --overwrite runs mkfs against the device and the pool comes up:
sudo virsh pool-build "${POOL_NAME}" --overwrite
sudo virsh pool-start "${POOL_NAME}"
sudo virsh pool-autostart "${POOL_NAME}"
Check the result. Capacity should reflect the device, and allocation should be near zero on a fresh ext4:
sudo virsh pool-info "${POOL_NAME}"
The 2 MiB of allocation is ext4 metadata, not wasted space:
Name: guest_fs
UUID: c128ddf1-e126-4242-9d1f-419485f19b94
State: running
Persistent: yes
Autostart: yes
Capacity: 9.74 GiB
Allocation: 2.02 MiB
Available: 9.74 GiB
Here is the full sequence as it runs on the host, from define through to the mount check:

Two checks prove the device is genuinely mounted rather than the pool quietly writing into a local directory. The first is the mount table:
mount | grep "${POOL_TARGET}"
The ext4 mount with libvirt’s enforced options:
/dev/vdc1 on /var/lib/libvirt/guest_fs type ext4 (rw,nosuid,nodev,noexec,relatime)
The second is lost+found. A freshly created ext4 filesystem always has one, so its presence in the target directory means you are looking at the device and not at an empty folder on the root filesystem:
sudo ls -la "${POOL_TARGET}"
One directory, owned by root:
total 24
drwxr-xr-x 3 root root 4096 Aug 9 20:07 .
drwxr-xr-x 7 root root 4096 Aug 9 20:07 ..
drwx------ 2 root root 16384 Aug 9 20:07 lost+found
Create volumes
This is where the filesystem pool pulls away from its closest relative. Volumes here are ordinary files on an ordinary filesystem, so they take the name you give them and the image format you ask for:
sudo virsh vol-create-as "${POOL_NAME}" web06-data.qcow2 5G --format qcow2
sudo virsh vol-create-as "${POOL_NAME}" archive.raw 2G --format raw
Both succeed, and the details view shows the difference between the two formats:
sudo virsh vol-list "${POOL_NAME}" --details
The qcow2 volume claims 5 GiB and occupies 196 KiB. The raw volume takes its full 2 GiB immediately:
Name Path Type Capacity Allocation
-----------------------------------------------------------------------------------------------
archive.raw /var/lib/libvirt/guest_fs/archive.raw file 2.00 GiB 2.00 GiB
lost+found /var/lib/libvirt/guest_fs/lost+found dir 0.00 B 0.00 B
web06-data.qcow2 /var/lib/libvirt/guest_fs/web06-data.qcow2 file 5.00 GiB 196.00 KiB
The same numbers confirmed against the image itself, where qemu-img reports a 5 GiB virtual size backed by 196 KiB on disk:

Thin provisioning is the practical consequence. Seven GiB of declared volume capacity fits on a 9.74 GiB pool with room left over, because only the qcow2 clusters that hold data are ever written. Overcommit deliberately and watch Available, since the pool runs out of real space long before the guests think their disks are full.
Set against the disk pool, which hands libvirt an entire block device and carves partitions out of it, the trade is straightforward:
| Behaviour | Filesystem pool | Disk pool |
|---|---|---|
| Volume is a | File on ext4/xfs | Partition on the device |
| Custom volume names | Yes | No, forced to vdb1, vdb2 |
| qcow2 supported | Yes | No, raw partitions only |
| Thin provisioning | Yes, with qcow2 | No, allocation equals capacity |
| Snapshots and backing files | Yes | No |
| Volume limit | Filesystem inode count | 128 GPT partitions |
| Space after deleting a volume | Returned to the pool | Leaves a gap in the partition table |
The filesystem layer costs a little throughput against raw partitions, which is why the LVM pool still has a place for latency sensitive guests, and why an iSCSI pool or an NFS export is the answer when more than one host needs the same images. For a single host that wants qcow2 on dedicated storage, the filesystem pool is the least complicated option that does the job.
Attach a volume to a running guest
Volumes on this pool attach by path, with the image format declared explicitly so QEMU does not have to guess:
sudo virsh attach-disk web02 "${POOL_TARGET}/web06-data.qcow2" vdb \
--driver qemu --subdriver qcow2 --targetbus virtio --persistent
The --persistent flag writes the disk into the domain XML as well as the live guest, so it survives a shutdown. Verify from the host:
sudo virsh domblklist web02
The new device sits alongside the root disk and the cloud-init seed:
Target Source
------------------------------------------------------
vda /var/lib/libvirt/images/web02.qcow2
vdb /var/lib/libvirt/guest_fs/web06-data.qcow2
sda /var/lib/libvirt/images/web02-seed.iso
Inside the guest the volume is a plain virtio disk. Format and mount it the usual way:
sudo mkfs.ext4 -F /dev/vdb
sudo mkdir -p /srv/data
sudo mount /dev/vdb /srv/data
df -h /srv/data
The guest sees 4.9 GB usable out of the 5 GiB volume:
Filesystem Size Used Avail Use% Mounted on
/dev/vdb 4.9G 1.3M 4.6G 1% /srv/data
Back on the host, the effect of that guest side format is visible in the allocation figure. Writing an ext4 filesystem into a 5 GiB thin volume grew it from 196 KiB to 5.26 MiB, which is the only space the pool has actually given up:
sudo virsh vol-info web06-data.qcow2 --pool "${POOL_NAME}"
Capacity unchanged, allocation tracking real usage:
Name: web06-data.qcow2
Type: file
Capacity: 5.00 GiB
Allocation: 5.26 MiB
Delete a volume and reclaim the space
Deleting is an unlink on a filesystem, so the space comes straight back with no fragmentation to reason about:
sudo virsh vol-delete archive.raw --pool "${POOL_NAME}"
sudo virsh pool-info "${POOL_NAME}" | grep -E 'Capacity|Allocation|Available'
Removing the 2 GiB raw volume returned the pool to 9.74 GiB available immediately:
Capacity: 9.74 GiB
Allocation: 7.28 MiB
Available: 9.74 GiB
A disk pool cannot do this. Deleting a partition from the middle of its table leaves a hole that only a small enough replacement can use. If you are cycling volumes of varying sizes, that difference decides which pool type you want. The virsh command reference collects the rest of the volume subcommands, including clone and resize.
What survives a host reboot
Filesystem pools introduce a mount that no other pool type has, and that mount is invisible to the tools most admins would check. It never appears in /etc/fstab. libvirtd performs it at pool start and unmounts it at pool stop, which means the answer to “will this come back on its own” depends entirely on the autostart flag.
Rebooting the lab host with autostart enabled returned everything without intervention. The pool was active, the device mounted, and both volumes intact:
Name State Autostart
-------------------------------------
guest_fs active yes
images active yes
kvm-disk-pool active yes
The guest disk attachment survived too, because it went in with --persistent. What did not come back were the guests themselves, since domain autostart is a separate setting from pool autostart. Enable it per guest with virsh autostart web02.
Skip pool-autostart and the failure is quieter than it should be. The target directory still exists after a reboot, empty and unmounted, and any guest whose disk lives inside it refuses to start:
error: Failed to start domain 'web02'
error: Cannot access storage file '/var/lib/libvirt/guest_fs/web06-data.qcow2' (as uid:64055, gid:991): No such file or directory
Nothing is lost. Run virsh pool-start guest_fs and the guest boots normally with its data where it was. The same message appears whenever a pool is stopped for maintenance, so treat it as “the mount is missing” rather than “the image is gone” and check virsh pool-list --all before reaching for a backup. On any host where guests depend on a filesystem pool, set autostart at the same time you create it, then reboot once on purpose to prove the whole chain works before it matters. That single test is worth more than any amount of reading the pool XML, and it pairs naturally with the cloud-init provisioning workflow if you are rebuilding guests on this storage regularly.