Resizing a Linux Filesystem Live with LVM (No Downtime)

df -h shows 100% and the app is still serving traffic. If the disk sits on LVM, you can almost always grow it without stopping anything: add space to the volume group, extend the logical volume, then grow the filesystem on top. The three steps are independent operations and mixing up their order is the most common way this goes wrong, so this walks through each one and what confirms it actually worked before moving to the next.

Check what you're actually working with first #

bash.bash

$ df -h /data
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/vg_data-lv_data 50G 50G 0 100% /data

$ lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
sda 8:0 0 100G 0 disk
├─sda1 8:1 0 1G 0 part /boot
└─sda2 8:2 0 99G 0 part
└─vg_data-lv_data 253:0 0 50G 0 lvm /data

$ vgs
VG #PV #LV #SN Attr VSize VFree
vg_data 1 1 0 wz--n- 99.00g 49.00g

That last line is the answer to "do I even need a new disk": VFree shows 49G already sitting unused in the volume group. Nothing needs to be attached. The space is there, it's just not allocated to the logical volume yet.

If VFree were closer to 0, you'd attach a new disk or cloud volume, then bring it into the volume group before continuing:

bash.bash

$ sudo pvcreate /dev/sdb
$ sudo vgextend vg_data /dev/sdb

Extend the logical volume #

bash.bash

$ sudo lvextend -L +40G /dev/vg_data/lv_data
Size of logical volume vg_data/lv_data changed from 50.00 GiB to 90.00 GiB.
Logical volume vg_data/lv_data successfully resized.

-L +40G adds 40G to the current size. To use everything free in the volume group instead of a fixed number, use -l +100%FREE:

bash.bash

$ sudo lvextend -l +100%FREE /dev/vg_data/lv_data

At this point the block device is bigger, but the filesystem sitting on it has no idea; df still reports the old size. This is the step people stop at and then wonder why nothing changed.

Grow the filesystem to match #

The command depends on the filesystem type, and both of these work on a mounted, live filesystem:

bash.bash

# ext4
$ sudo resize2fs /dev/vg_data/lv_data
Filesystem at /dev/vg_data/lv_data is mounted on /data; on-line resizing required
resize2fs 1.46.5 (30-Dec-2021)
The filesystem on /dev/vg_data/lv_data is now 23592960 (4k) blocks long.

# XFS note: resize takes the *mount point*, not the block device
$ sudo xfs_growfs /data

That distinction trips people up constantly: resize2fs targets the device, xfs_growfs targets the mount point. Running xfs_growfs against a device path fails immediately, which is a useful sanity check that you've got the right tool for the filesystem in front of you.

Confirm before you move on #

bash.bash

$ df -h /data
Filesystem Size Used Avail Use% Mounted on
/dev/mapper/vg_data-lv_data 89G 50G 35G 57% /data

Don't trust the previous step's "success" output alone. df on the live mount is the real confirmation. If it still shows the old size after resize2fs/xfs_growfs reported success, the mount you resized and the mount you're checking may not be the same device (common with bind mounts or a stale NFS cache); recheck lsblk and mount | grep /data before doing anything else.

Shrinking is a different, riskier operation #

Everything above only grows. Shrinking works in the opposite order (filesystem first, then logical volume), and it is much easier to lose data if you get it wrong:

XFS cannot be shrunk at all, online or offline. The only path is backup, recreate, restore.

ext4 can be shrunk, but only unmounted, and only with resize2fs given an explicit target size smaller than the current filesystem, run before lvreduce, never after. Shrinking the logical volume before the filesystem has been told to release that space truncates live data.

bash.bash

# ext4 shrink: offline, filesystem first, LV second, in that order
$ sudo umount /data
$ sudo e2fsck -f /dev/vg_data/lv_data
$ sudo resize2fs /dev/vg_data/lv_data 40G
$ sudo lvreduce -L 40G /dev/vg_data/lv_data
$ sudo mount /data

If a filesystem needs to shrink and it's XFS, there usually isn't a clean answer short of migrating the data to a smaller volume. Plan for that with headroom rather than discovering it during an incident.

Snapshot before anything that isn't a pure grow #

LVM snapshots are cheap and this is exactly the scenario they exist for:

bash.bash

$ sudo lvcreate -L 5G -s -n lv_data_snap /dev/vg_data/lv_data
$ # ... do the risky operation ...
$ sudo lvremove /dev/vg_data/lv_data_snap # once confirmed good

A snapshot isn't a substitute for a real backup (it lives on the same volume group and disappears if the underlying storage fails), but it's a fast rollback path for "this shrink or filesystem operation went wrong five minutes ago," which covers most of the actual risk in these commands.

The fix in order #

Check vgs for free space before touching anything; you may not need a new disk at all.

Grow: lvextend on the logical volume, then resize2fs (ext4) or xfs_growfs (XFS, mount point not device) on the filesystem. Both work live.

Confirm with df -h on the actual mount, not just the command's own success message.

Shrink (ext4 only, XFS can't): unmount, resize2fs to the smaller target first, lvreduce second, since reversing that order risks data loss.

Snapshot before any shrink or filesystem-type-specific operation you haven't run before on this box.

This pairs with diagnosing disk full on Linux when the question is what's eating the space in the first place, not just how to add more of it.

The call we'd make #

Keep 15-20% headroom in the volume group on any disk-sensitive box so a grow is always the lvextend + resize path above: five minutes, zero downtime, reversible via snapshot if something looks wrong. Treat shrinking as a planned migration, not a live operation, especially on XFS where it's not an option at all.