You resize a root partition without LVM in three separate steps, and skipping any one of them leaves you staring at a full / while lsblk insists the disk got bigger. The virtual disk grows at the hypervisor. The partition grows with growpart. The filesystem grows with resize2fs on ext4 or xfs_growfs on XFS. All of it happens online, on a mounted root, with no reboot and no rescue ISO.
This walks the full chain on a plain GPT layout with no volume manager in the way, including the failure messages that stop people at step two. Every command run inside the guests below was executed in September 2026 on Ubuntu 26.04.1 LTS with an ext4 root and Rocky Linux 10.2 with an XFS root.
Check what you are actually resizing
Find the device behind / and its filesystem type before touching anything. The filesystem type decides which resize command you run, and it is not always what the distro name suggests.
findmnt -no SOURCE,FSTYPE /
lsblk
df -hT /
On the Ubuntu host the root filesystem is ext4 on /dev/vda1, and the disk has already been grown to 30G at the hypervisor while the partition still sits at its original size:
/dev/vda1 ext4
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
vda 253:0 0 30G 0 disk
├─vda1 253:1 0 18.9G 0 part /
├─vda13 253:13 0 1023M 0 part /boot
├─vda14 253:14 0 4M 0 part
└─vda15 253:15 0 106M 0 part /boot/efi
Filesystem Type Size Used Avail Use% Mounted on
/dev/vda1 ext4 19G 3.0G 16G 16% /
Note the partition numbering. Root is partition 1 but it is physically the last partition on the disk, sitting after 13, 14 and 15. That ordering is what matters, not the number. The Rocky host is more conventional, with XFS on the fourth and final partition:
/dev/sda4 xfs
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sda 8:0 0 20G 0 disk
├─sda1 8:1 0 2M 0 part
├─sda2 8:2 0 200M 0 part /boot/efi
├─sda3 8:3 0 1000M 0 part /boot
└─sda4 8:4 0 8.8G 0 part /
Filesystem Type Size Used Avail Use% Mounted on
/dev/sda4 xfs 8.4G 1.7G 6.8G 20% /
Two facts to carry forward: the device name (/dev/vda1 or /dev/sda4) and the filesystem type. If lsblk shows an lvm line under the partition, stop here. That machine has a volume manager and the procedure is different, covered in the LVM root filesystem guide instead.
Extend the virtual disk first
Nothing inside the guest can create space that the block device does not have. Grow the disk on the platform that owns it. On Proxmox VE, from the node shell:
qm resize 102 virtio0 +10G
On plain libvirt or KVM, find the target name and grow the device while the guest keeps running. qemu-img resize is the offline path only, because QEMU holds the write lock on the image for as long as the guest is up:
sudo virsh domblklist web01
sudo virsh blockresize web01 vda 30G
On AWS you modify the EBS volume rather than the instance, which the EBS boot disk walkthrough covers end to end, and the offline qcow2 route lives in the qemu-img reference.
Back in the guest, the kernel notices the new capacity on its own with virtio and virtio-scsi. No reboot, no rescan:
sudo dmesg | tail -3
Both hosts logged the change within a second of the hypervisor command. Ubuntu on virtio-blk:
virtio_blk virtio2: [vda] new size: 62914560 512-byte logical blocks (32.2 GB/30.0 GiB)
vda: detected capacity change from 41943040 to 62914560
Rocky on virtio-scsi:
sd 0:0:0:0: Capacity data has changed
sd 0:0:0:0: [sda] 41943040 512-byte logical blocks: (21.5 GB/20.0 GiB)
sda: detected capacity change from 20971520 to 41943040
If lsblk still shows the old disk size, the controller missed the event. SCSI-backed disks, which covers virtio-scsi, SATA, SAS and most cloud block devices, expose a per-device rescan node, so poke that one device instead of rebooting. Substitute your own disk name:
echo 1 | sudo tee /sys/class/block/sda/device/rescan
echo "- - -" | sudo tee /sys/class/scsi_host/host0/scan
That node belongs to the SCSI layer. Virtio-blk disks such as /dev/vda have no rescan node at all, so on the Ubuntu host /sys/class/block/vda/device/rescan does not exist and there is nothing to poke from inside the guest. If a virtio-blk guest misses the capacity change, the disk has to be detached and reattached at the hypervisor, or the guest restarted.
Install growpart if it is missing
growpart rewrites a single partition entry so it fills the free space that follows it. It is a shell script wrapped around sfdisk, with the awkward parts already handled: it forces the write, backs up the old table first, and pushes the new size into the kernel when the re-read ioctl fails on a mounted root. Check before installing, because cloud images ship with it already:
command -v growpart
Both test hosts answered /usr/bin/growpart straight out of the image. On a minimal or bare metal install you will need the package. Debian and Ubuntu ship it in cloud-guest-utils:
sudo apt update
sudo apt -y install cloud-guest-utils gdisk
RHEL, Rocky Linux and AlmaLinux ship it as cloud-utils-growpart in AppStream, with no EPEL needed. Fedora carries the same package name in its main repository:
sudo dnf -y install cloud-utils-growpart gdisk
Confirm which repository served it if you are auditing a locked-down build:
dnf repoquery --qf '%{name} %{version}-%{release} %{reponame}' cloud-utils-growpart
On Rocky Linux 10 that returns the base channel:
cloud-utils-growpart 0.33-11.el10 appstream
Grow the partition with growpart
Run the dry run first. The -N flag prints the old and new sfdisk dumps on stderr and writes nothing, so you can see exactly which entry is about to move while the CHANGE: line stays on stdout:
sudo growpart -N /dev/vda 1
The disk and the partition number are separate arguments, with no p and no partition suffix. The first line of the dry run is the summary worth reading:
CHANGE: partition=1 start=2324480 old: size=39618527 end=41943006 new: size=60590047 end=62914526
The start sector is identical in both columns. That is the safety property that makes this survivable on a live root: growpart only ever moves the end of a partition, so no data block changes address. Drop the -N to apply it:
sudo growpart /dev/vda 1
A successful run reports CHANGED and the kernel picks up the new partition size immediately:
CHANGED: partition=1 start=2324480 old: size=39618527 end=41943006 new: size=60590047 end=62914526
There is a second repair happening here that is easy to miss. Growing a GPT disk leaves the backup partition table stranded in the middle of the device, and every partitioning tool complains about it. Before growpart, sgdisk -v /dev/vda reported:
Problem: The secondary header's self-pointer indicates that it doesn't reside
at the end of the disk. If you've added a disk to a RAID array, use the 'e'
option on the experts' menu to adjust the secondary header's and partition
table's locations.
Identified 1 problems!
After the run, the same check is clean, because writing the table puts the backup header back at the end of the now larger disk:
No problems found. 4061 free sectors (2.0 MiB) available in 2
segments, the largest of which is 2047 (1023.5 KiB) in size.
Confirm the partition grew, and note that df has not moved yet:
lsblk /dev/vda
df -hT /
The partition is 28.9G, the filesystem inside it is still 19G. This gap is the single most common point of confusion:
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
vda 253:0 0 30G 0 disk
├─vda1 253:1 0 28.9G 0 part /
├─vda13 253:13 0 1023M 0 part /boot
├─vda14 253:14 0 4M 0 part
└─vda15 253:15 0 106M 0 part /boot/efi
Filesystem Type Size Used Avail Use% Mounted on
/dev/vda1 ext4 19G 2.9G 16G 16% /
Resize an ext4 root filesystem with resize2fs
resize2fs handles ext2, ext3 and ext4 identically. Point it at the device, not the mount point, and give it no size argument so it grows to fill the partition:
sudo resize2fs /dev/vda1
The “on-line resizing required” line is informational, not an error. It means the filesystem is mounted and the kernel is doing the work in place:
resize2fs 1.47.2 (1-Jan-2025)
Filesystem at /dev/vda1 is mounted on /; on-line resizing required
old_desc_blocks = 3, new_desc_blocks = 4
The filesystem on /dev/vda1 is now 7573755 (4k) blocks long.
The space is live immediately:
Filesystem Type Size Used Avail Use% Mounted on
/dev/vda1 ext4 28G 2.9G 26G 11% /
Nothing needs editing in /etc/fstab afterwards. The entry references a LABEL or UUID and neither changes when a partition grows, so the mount survives a reboot untouched.

Resize an XFS root filesystem with xfs_growfs
The partition step is identical, only the number changes. On the Rocky host root is the fourth partition:
sudo growpart /dev/sda 4
Same CHANGED line, same untouched start sector:
CHANGED: partition=4 start=2463744 old: size=18507743 end=20971486 new: size=39479263 end=41943006
XFS diverges at the filesystem step. Historically xfs_growfs took the mount point rather than the device, and the mount point still works everywhere:
sudo xfs_growfs /
It prints the filesystem geometry, then the one line that matters, the block count change:
meta-data=/dev/sda4 isize=512 agcount=4, agsize=578367 blks
= sectsz=512 attr=2, projid32bit=1
= crc=1 finobt=1, sparse=1, rmapbt=1
data = bsize=4096 blocks=2313467, imaxpct=25
log =internal log bsize=4096 blocks=115425, version=2
realtime =none extsz=4096 blocks=0, rtextents=0
data blocks changed from 2313467 to 4934907
xfsprogs has accepted a mounted device node again since 5.3, so sudo xfs_growfs /dev/sda4 does the same thing. The mount point is the form that works on every version. Either way the result is live with no remount:
Filesystem Type Size Used Avail Use% Mounted on
/dev/sda4 xfs 19G 1.5G 17G 8% /
The same two commands on the Rocky host, start to finish:

That is the whole difference between the two filesystems in this workflow:
| Filesystem | Grow command | Argument | Online grow | Shrink |
|---|---|---|---|---|
| ext2, ext3, ext4 | resize2fs | device | yes | offline only |
| XFS | xfs_growfs | mount point or device | yes | last AG only, experimental |
If the volume you are growing sits on a logical volume, or the filesystem is Btrfs, the command set changes again. Those cases are handled in the XFS and Btrfs resize guide.
Grow the partition without growpart
Some hardened images will not let you add packages, and growpart is a shell script that has to be present. The Rocky disk was grown by another 5G before this section, so the numbers below start from 25G rather than the 20G used earlier. The instinct is to reach for parted, which does not work on a mounted root in script mode:
sudo parted -f -s /dev/sda resizepart 4 100%
It fixes the GPT header, then refuses the resize and exits 1 because the partition is in use. The prompt it wants answered never appears under -s:
Warning: Not all of the space available to /dev/sda appears to be used, you can
fix the GPT to use all of the space (an extra 10485760 blocks) or continue with
the current setting?
Fixing, due to --fix
Warning: Partition /dev/sda4 is being used. Are you sure you want to continue?
sfdisk is the one that works. Feeding it , + means “keep the start, extend the size to the end of the disk”, and --no-reread skips the pre-flight check that would refuse to touch a disk the kernel reports as in use:
echo ', +' | sudo sfdisk -N 4 /dev/sda --no-reread --force
It prints the new entry, writes the change, and then warns that the running kernel is still on the old view:
Device Start End Sectors Size Type
/dev/sda4 2463744 52428766 49965023 23.8G Linux root (x86-64)
The partition table has been altered.
Calling ioctl() to re-read partition table.
Re-reading the partition table failed.: Device or resource busy
The kernel still uses the old table. The new table will be used at the next reboot or after you run partprobe(8) or partx(8).
Syncing disks.
That last warning is expected and not a failure. Push the new sizes into the running kernel with partx, which updates partition entries one at a time instead of demanding a re-read of the whole table. Add --nr 4 to touch only the partition you changed:
sudo partx -u /dev/sda
lsblk then shows 23.8G on sda4 and xfs_growfs or resize2fs finishes the job exactly as before. This is also the answer when a tool wrote the table but the kernel never caught up, which the older no tools available to resize disk with gpt note covers from the other direction.
When growpart says NOCHANGE
growpart exits 1 and prints NOCHANGE rather than explaining itself. There are three distinct reasons behind the same word, and the exact wording tells you which one you hit.
NOCHANGE: partition 1 is size 60590047. it cannot be grown
There is no free space after the partition. Either the disk was never extended at the hypervisor, or the guest has not seen the new capacity yet. Compare the disk line and the partition line:
lsblk /dev/vda
If the disk and the partition report the same size, the growth already happened. If the disk is still the old size, go back to the hypervisor step.
NOCHANGE: partition 1 could only be grown by 1024 [fudge=2048]
There is free space, but less than the fudge factor, so growpart decides the change is not worth writing a partition table for. The default fudge is 1048576 bytes, printed here as 2048 sectors, and the number before it is what you would actually gain. Reproduced on a loop device with roughly 512 KiB of free tail:
NOCHANGE: partition 1 could only be grown by 1024 [fudge=2048]
Gaining half a megabyte on a root filesystem is pointless, so the right fix is almost always to grow the disk by a real amount and run it again. Lowering the threshold with --fudge is possible but rarely what you want.
NOCHANGE on a partition that is not last on the disk
This one produces the same “it cannot be grown” wording and catches people out because the disk clearly has free space. growpart can only expand into space that is immediately after the partition, and it will not move a neighbour to get there. A layout with swap sitting behind root hits it every time:
Device Start End Sectors Size Type
/dev/loop0p1 2048 4196351 4194304 2G Linux filesystem
/dev/loop0p2 4196352 6293503 2097152 1G Linux swap
After doubling the disk, partition 1 refuses while partition 2 succeeds, because only partition 2 touches the free space:
NOCHANGE: partition 1 is size 4194304. it cannot be grown
CHANGED: partition=2 start=4196352 old: size=2097152 end=6293503 new: size=12580831 end=16777182
The fix is to delete the partition in the way. With swap that is cheap: swapoff -a, remove the swap partition, comment its line out of /etc/fstab, grow root, then recreate swap at the end of the disk or replace it with a swap file. Anything holding real data has to be moved or backed up first.

Why resize2fs fails on an XFS root
resize2fs is an ext2/3/4 tool and has no XFS support at all, but the error it throws does not say so. Pointed at an XFS partition it reports a superblock problem, which reads like corruption:
sudo resize2fs /dev/sda4
Nothing is damaged. It looked for an ext superblock, found XFS metadata, and gave up:
resize2fs 1.47.1 (20-May-2024)
resize2fs: Bad magic number in super-block while trying to open /dev/sda4
Couldn't find valid filesystem superblock.
Check the type with findmnt -no FSTYPE / and use xfs_growfs. The reverse mistake is harmless in the same way, since xfs_growfs refuses an ext4 target rather than touching it.
Shrinking is a different story
Everything above only grows. Shrinking splits sharply by filesystem, and this is where a lot of resize plans die.
ext4 can shrink, but never while mounted. It needs the filesystem offline, an e2fsck -f first, then resize2fs /dev/vda1 20G, and only then can you shrink the partition to match. For a root filesystem that means booting a live ISO. Shrink the filesystem before the partition, and leave a margin, because a partition smaller than its filesystem destroys it.
XFS is the blunter case. Current xfsprogs can only give back unused space in the last allocation group, so any shrink that would remove an allocation group is refused. Taking this root from 4934907 blocks down to 1000000 crosses several of them:
sudo xfs_growfs -D 1000000 /
xfsprogs 6.16 on Rocky Linux 10 flags the attempt and then fails it:
[EXPERIMENTAL] try to shrink unused space 1000000, old size is 4934907
xfs_growfs: XFS_IOC_FSGROWFSDATA xfsctl failed: Invalid argument
Treat XFS as grow-only in production. To make an XFS root meaningfully smaller you back it up, make a new smaller filesystem, and restore. That asymmetry is worth knowing before you pick a filesystem for a machine whose disk you might want to trim later.
For the common case the sequence is short enough to memorise: grow the disk, run growpart on the disk and partition number, then resize2fs on the device for ext4 or xfs_growfs on the mount point for XFS. Verify with df -hT /. No reboot, no unmount, no fstab edit. If any step reports NOCHANGE, the answer is in the wording, not in a bigger hammer.
Related reading: extending a root filesystem on LVM, extending a KVM virtual machine disk, resizing guest images with virt-resize, and checking disk partitions and usage.
You are a life-saver man. Thanks for the amazing guides
Thanks and welcome
Thanks a lot mate!
Welcome!
Wonderful! This worked perfectly!
Work straight “out of the box” on my linux VM (ubuntu server 20.04) Thanks a lot 🙂
That’s awesome!