Growing a Linux disk live: growpart, ext4, XFS, LVM

After a move to a bigger plan, df often still shows the old size: the disk grew, the partition and the filesystem did not. ext4, XFS and LVM, each grown live on a test machine, with the real output.

Three stacked layers: the disk has grown, the partition and the filesystem stretch after it

You moved to a bigger plan, or you attached a larger disk, and df -h still shows the old size. The disk did grow. What sits on top of it did not: the partition, sometimes an LVM layer, then the filesystem. Each layer has to be extended in that order, and each one has its own command.

Everything below was run on a Debian 13 test machine with three disks that we enlarged while it was running: a root partition in ext4, a data disk in XFS and a data disk under LVM. The outputs are the real ones, and none of it needs a reboot.

1. Often, a reboot is enough

Most of our images grow their root partition on their own: cloud-init runs growpart and resizes the filesystem every time the machine starts. Oracle Linux is the exception, because its root sits on LVM and cloud-init leaves LVM alone. Our Oracle Linux images add a small service that runs the whole chain at every boot. On a Starter (40 GB), the root comes up at 34.8 GB. Move the same machine to an 80 GB disk, reboot, and it reaches 74.8 GB.

If you can afford a reboot, try that first. The rest of this article is for when you can't, or when the space belongs to a second disk or to an LVM layout you set up yourself.

2. Find where the space is stuck

lsblk shows every layer with its size. Right after the three disks grew, each disk is larger than the partition it holds:

bash
$ lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS /dev/sda /dev/sdb /dev/sdc
NAME                SIZE TYPE FSTYPE      MOUNTPOINTS
sda                  12G disk
├─sda1              5.9G part ext4        /
├─sda14               3M part
└─sda15             124M part vfat        /boot/efi
sdb                   6G disk
└─sdb1                2G part xfs         /srv/data
sdc                   6G disk
└─sdc1                2G part LVM2_member
  └─vg_data-lv_srv    2G lvm  xfs         /srv/lvm

df -h confirms that the filesystems have not moved:

bash
$ df -h / /srv/data /srv/lvm
Filesystem                  Size  Used Avail Use% Mounted on
/dev/sda1                   5.8G  915M  4.6G  17% /
/dev/sdb1                   2.0G   71M  1.9G   4% /srv/data
/dev/mapper/vg_data-lv_srv  2.0G   71M  1.9G   4% /srv/lvm

Then ask findmnt what holds the mount point you want to grow, and in which format. The answer tells you which case you are in:

3. Install growpart

growpart rewrites the partition table so that a partition takes all the free space that follows it. It is usually already there on cloud images, since cloud-init relies on it. Otherwise, the package name depends on the family:

The filesystem tools come from e2fsprogs (resize2fs), xfsprogs (xfs_growfs) and lvm2.

4. Case 1: a plain partition in ext4

Before touching anything, fdisk -l may greet you with this:

bash
GPT PMBR size mismatch (12582911 != 25165823) will be corrected by write.
The backup GPT table is not on the end of the device.

GPT keeps a backup copy of the partition table in the last sectors of the disk. When the disk grows, that copy is no longer at the end. It is harmless, and growpart moves the copy when it rewrites the table: after the next step, fdisk no longer complains.

The disk and the partition number are two separate arguments. Start with --dry-run, which shows the change without writing it:

bash
$ sudo growpart --dry-run /dev/sda 1
CHANGE: partition=1 start=262144 old: size=12320735 end=12582878 new: size=24903647 end=25165790
$ sudo growpart /dev/sda 1
CHANGED: partition=1 start=262144 old: size=12320735 end=12582878 new: size=24903647 end=25165790

The partition now covers the disk, but the filesystem still has its old size. resize2fs grows a mounted ext4 in place:

bash
$ sudo resize2fs /dev/sda1
resize2fs 1.47.2 (1-Jan-2025)
Filesystem at /dev/sda1 is mounted on /; on-line resizing required
old_desc_blocks = 1, new_desc_blocks = 2
The filesystem on /dev/sda1 is now 3112955 (4k) blocks long.

$ df -h /
Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1        12G  915M   11G   9% /

Running growpart again does no harm. It reports that there is nothing left to take:

bash
$ sudo growpart /dev/sda 1
NOCHANGE: partition 1 is size 24903647. it cannot be grown

growpart can only extend a partition into the free space right after it. On this Debian image, partition 1 is the last one on the disk (partitions 14 and 15 sit at the start), so it has room. If another partition sits behind yours, you also get NOCHANGE, and that partition has to be moved or removed first.

5. Case 2: XFS

XFS is the default filesystem of the Red Hat family. The first step is the same. The second differs: xfs_growfs takes the mount point, not the device, and only works on a mounted filesystem.

bash
$ sudo growpart /dev/sdb 1
CHANGED: partition=1 start=2048 old: size=4190208 end=4192255 new: size=12580831 end=12582878
$ sudo xfs_growfs /srv/data
[…]
data blocks changed from 523776 to 1572603

$ df -h /srv/data
Filesystem      Size  Used Avail Use% Mounted on
/dev/sdb1       6.0G  150M  5.8G   3% /srv/data

xfs_growfs prints the whole filesystem geometry before that last line; we cut it here.

6. Case 3: LVM

LVM adds a layer between the partition and the filesystem. The partition is a physical volume (PV), the PVs form a volume group (VG), and the VG is cut into logical volumes (LV). After growpart, the PV has to be told about its new size, which gives the group free space:

bash
$ sudo vgs
  VG      #PV #LV #SN Attr   VSize  VFree
  vg_data   1   1   0 wz--n- <2.00g    0
$ sudo growpart /dev/sdc 1
CHANGED: partition=1 start=2048 old: size=4190208 end=4192255 new: size=12580831 end=12582878
$ sudo pvresize /dev/sdc1
  Physical volume "/dev/sdc1" changed
  1 physical volume(s) resized or updated / 0 physical volume(s) not resized
$ sudo vgs
  VG      #PV #LV #SN Attr   VSize  VFree
  vg_data   1   1   0 wz--n- <6.00g 4.00g

lvextend -r hands that free space to the logical volume and grows the filesystem in the same command, whether it is ext4 or XFS:

bash
$ sudo lvextend -r -l +100%FREE /dev/vg_data/lv_srv
  File system xfs found on vg_data/lv_srv mounted at /srv/lvm.
  Size of logical volume vg_data/lv_srv changed from <2.00 GiB (511 extents) to <6.00 GiB (1535 extents).
  Extending file system xfs to <6.00 GiB (6438256640 bytes) on vg_data/lv_srv...
[…]
  Extended file system xfs on vg_data/lv_srv.
  Logical volume vg_data/lv_srv successfully resized.

$ df -h /srv/lvm
Filesystem                  Size  Used Avail Use% Mounted on
/dev/mapper/vg_data-lv_srv  6.0G  150M  5.8G   3% /srv/lvm

A root on LVM follows the same path. findmnt / gives the logical volume and pvs gives the partition under it. On our Oracle Linux images, that is partition 4:

bash
sudo growpart /dev/sda 4
sudo pvresize /dev/sda4
sudo lvextend -r -l +100%FREE /dev/mapper/vg_main-lv_root

7. The other direction

Growing works live. Shrinking does not. XFS cannot be shrunk: recent versions of xfs_growfs carry an experimental attempt, and on our test machine it failed:

bash
$ sudo xfs_growfs -D 1000000 /srv/data
[…]
[EXPERIMENTAL] try to shrink unused space 1000000, old size is 1572603
xfs_growfs: XFS_IOC_FSGROWFSDATA xfsctl failed: Invalid argument

ext4 can shrink, but only when it is not mounted, which rules out a running root:

bash
$ sudo resize2fs /dev/sda1 8G
resize2fs 1.47.2 (1-Jan-2025)
Filesystem at /dev/sda1 is mounted on /; on-line resizing required
resize2fs: On-line shrinking not supported

To end up with a smaller filesystem, you create a new one at the size you want and copy the data across.

8. Before you type

These commands rewrite a partition table on a running system. They are routine, but a wrong device name hits the wrong disk. Take a backup from the console first: every plan includes 7 manual backups. Read the old and new sizes that growpart --dry-run prints before you drop the option. And check the device name twice, since sda and sdb differ by one letter.

Stacked disk images, one of them flagged with a warning sign

Cloud images for 28 systems: 8 traps we fixed

28 operating systems, each from its distribution's own cloud image, each with its own habits. The traps that cost us time, what you see when one of them bites, and the fix our images now carry.

Read the article 7 min read

A workflow hands a job to a fresh machine, timed by a stopwatch, with its cache beside it; the machines of earlier jobs are already gone

GitHub Actions runners: half the price, measured

FFxF's GitHub Actions runners are open to everyone, in beta. The same workflow run at GitHub and at FFxF: a slightly faster build, half the bill, a slower cold docker build and 4 seconds with the Docker cache.

Read the article 7 min read

Support & discussions

Technical questions, incident reports, or infrastructure discussions, the team is reachable on Discord, Telegram, X, Instagram, Reddit, and IRC.