Storage, Disks, and LVM¶
What You'll Learn¶
- How block devices, partitions, filesystems, and mounts fit together
- How to add a disk and mount it persistently and safely
- How LVM makes volumes flexible, and how to grow a cloud disk without downtime
- When a server should have swap, and how to add a swap file
- How to diagnose and fix "No space left on device", including inode exhaustion and deleted files
Mental Model¶
flowchart TD
D["Block device<br/>/dev/nvme1n1"] --> P["Partition<br/>/dev/nvme1n1p1"]
P --> PV["LVM physical volume"]
PV --> VG["Volume group: data"]
VG --> LV["Logical volume: /dev/data/app"]
LV --> FS["Filesystem: ext4 or XFS"]
FS --> M["Mount point: /var/lib/app"]
LVM is optional — you can put a filesystem straight on a partition or a whole disk — but each layer adds a capability: partitions divide a disk, LVM lets volumes span and grow across disks, and the filesystem organizes files.
See What You Have¶
lsblk -f # devices, partitions, filesystems, UUIDs, mount points
df -hT # mounted filesystems: size, used, type
df -i # inode usage
findmnt /var/lib/docker # which filesystem a path lives on, and its options
sudo blkid # UUIDs and filesystem types
On cloud instances, NVMe device names (nvme0n1, nvme1n1) can change order between boots. That's why mounts should use UUIDs, never device names.
Add and Mount a New Disk¶
# 1. Identify the new, empty disk
lsblk
# 2. Create a filesystem (whole disk, no partition, for a simple data volume)
sudo mkfs.xfs -L appdata /dev/nvme1n1 # or: sudo mkfs.ext4 -L appdata /dev/nvme1n1
# 3. Mount it
sudo mkdir -p /var/lib/app
sudo mount /dev/nvme1n1 /var/lib/app
# 4. Make it persistent by UUID
UUID=$(sudo blkid -s UUID -o value /dev/nvme1n1)
echo "UUID=$UUID /var/lib/app xfs defaults,nofail 0 2" | sudo tee -a /etc/fstab
# 5. Test fstab BEFORE rebooting
sudo umount /var/lib/app
sudo mount -a && findmnt /var/lib/app
nofail lets the machine boot even if this volume is missing. Without it, a detached data disk can drop a cloud instance into emergency mode with no SSH access. If that happens, Fix a Broken fstab walks through the recovery.
ext4 or XFS?¶
| ext4 | XFS | |
|---|---|---|
| Default on | Ubuntu, Debian | RHEL, Rocky, Amazon Linux |
| Grow online | Yes (resize2fs) |
Yes (xfs_growfs) |
| Shrink | Yes (offline) | No |
| Strength | Flexible, familiar | Large files, parallel I/O |
LVM¶
sudo apt install -y lvm2
# Build: physical volume → volume group → logical volume
sudo pvcreate /dev/nvme2n1
sudo vgcreate data /dev/nvme2n1
sudo lvcreate -n app -L 50G data
sudo mkfs.ext4 /dev/data/app
# Inspect
sudo pvs; sudo vgs; sudo lvs
# Add another disk to the group and grow the volume and filesystem in one step
sudo pvcreate /dev/nvme3n1
sudo vgextend data /dev/nvme3n1
sudo lvextend -r -L +100G /dev/data/app # -r resizes the filesystem too
LVM snapshots (lvcreate -s) give a point-in-time copy for a quick backup or a safe upgrade, but they slow writes while they exist. Remove them promptly.
Grow a Cloud Disk Without Downtime¶
After increasing the volume size in the cloud console, API, or Terraform:
lsblk # the disk shows the new size; the partition doesn't yet
# Root disk with a partition: grow the partition, then the filesystem
sudo growpart /dev/nvme0n1 1 # from cloud-guest-utils
sudo resize2fs /dev/nvme0n1p1 # ext4
# or
sudo xfs_growfs / # XFS takes the mount point
df -h /
For a whole-disk filesystem with no partition, skip growpart. For LVM, run pvresize /dev/nvme1n1 and then lvextend -r -l +100%FREE.
Swap¶
Swap is disk space the kernel can move rarely used memory pages to. A little swap gives a server room to absorb a short memory spike instead of OOM-killing a process. A lot of active swapping makes everything slow (see Performance Troubleshooting).
Many cloud images ship with no swap. Add a swap file:
sudo dd if=/dev/zero of=/swapfile bs=1M count=2048 status=progress # 2 GiB
sudo chmod 600 /swapfile # swap can hold secrets from memory
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap defaults 0 0' | sudo tee -a /etc/fstab
swapon --show
dd writes every block, which works on every filesystem. fallocate is faster but can create files with holes that swapon rejects on some filesystems. Btrfs needs extra steps (btrfs filesystem mkswapfile).
How eagerly the kernel swaps is set by vm.swappiness; see Kernel Tuning. To remove a swap file, run sudo swapoff /swapfile, delete its fstab line, then delete the file.
| Workload | Swap guidance |
|---|---|
| General-purpose server | A small swap file (1–4 GiB) as a buffer, with alerts on sustained swap activity |
| Databases and latency-sensitive services | Often none, or very low swappiness; follow the database's own guidance |
| Kubernetes nodes | The kubelet refuses to start with swap on unless configured for it (failSwapOn: false); newer releases support swap with the LimitedSwap behavior. Check the documentation for your version. |
Fedora and some other distributions use zram instead: swap held in compressed RAM, which absorbs spikes without disk I/O. Check with zramctl.
"No Space Left on Device"¶
Work through these in order:
# 1. Which filesystem is full?
df -hT
# 2. What's big on it? (-x stays on one filesystem)
sudo du -xh --max-depth=1 /var | sort -h | tail
sudo find /var -xdev -type f -size +500M -exec ls -lh {} + 2>/dev/null
# 3. Is it inodes rather than bytes?
df -i
sudo find /var -xdev -type d -exec sh -c 'echo "$(ls -A "$1" | wc -l) $1"' _ {} \; 2>/dev/null | sort -n | tail
# 4. Deleted files still held open by a process
sudo lsof +L1
The deleted-but-open trap¶
If someone deletes a 20 GB log file that a process still has open, du stops counting it but df still shows the space as used — the blocks aren't freed until the file is closed.
sudo lsof +L1 | grep deleted
# nginx 1203 www-data 5w REG 259,1 21474836480 0 /var/log/nginx/access.log (deleted)
# Fix: restart or signal the process to reopen its logs
sudo systemctl reload nginx
# Emergency without a restart: truncate through the process's file descriptor
sudo truncate -s 0 /proc/1203/fd/5
Usual suspects¶
| Location | Cause | Fix |
|---|---|---|
/var/log |
Unrotated application logs | logrotate with copytruncate or a reload, size limits |
/var/log/journal |
No journald size limit | SystemMaxUse= in journald.conf |
/var/lib/docker |
Old images, stopped containers, build cache | docker system df, docker system prune |
/var/cache/apt |
Package cache | sudo apt clean |
/tmp |
Abandoned temporary files | Find the job creating them |
To avoid the incident entirely, put /var/lib/docker and application data on separate volumes, and alert when any filesystem passes 80%.
Common Mistakes¶
- Using
/dev/nvme1n1in/etc/fstabinstead of a UUID, then booting with devices in a different order. - Adding an fstab entry without
nofailand without testingmount -a, leaving the instance unbootable. - Deleting a large log file instead of truncating it or reloading the writer, so the space never comes back.
- Only checking
df -hand missing inode exhaustion from millions of tiny files. - Growing the cloud volume but forgetting to grow the partition and filesystem.
- Formatting the wrong disk — always confirm with
lsblk -fthat the target has no filesystem.
Interview Questions¶
dfsays a filesystem is full, butdufinds only half the space used. What's going on?- How do you add a data disk so it survives reboots and device renaming?
- Walk through growing the root filesystem of a cloud VM without downtime.
- What problem does LVM solve, and when would you not bother with it?
- What causes "No space left on device" when
df -hshows free space?
Next¶
Continue to Users, sudo, and SSH Hardening.