RHEL Basics #6: Filesystem Basics — XFS, mount, /etc/fstab

10 min read

The files we set permissions on in #5 live on a filesystem, which sits on a block device (a disk), mounted somewhere in the directory tree. This post covers that layer. By the end, we’ll have added a fresh disk and mounted it as a data directory.

Where this post sits in the RHEL Basics series:

XFS — the RHEL 9 default #

RHEL 9’s default filesystem is XFS (default since RHEL 7; before that, ext4).

currently mounted filesystems
$ findmnt -t xfs,ext4
TARGET   SOURCE                       FSTYPE OPTIONS
/        /dev/mapper/rhel-root        xfs    rw,relatime,attr2,inode64,...
/boot    /dev/vda1                    xfs    rw,relatime,attr2,inode64,...
/home    /dev/mapper/rhel-home        xfs    rw,relatime,attr2,inode64,...

XFS vs ext4 #

ItemXFSext4
RHEL default✅ (RHEL 7+)RHEL 6 era default
Large files / large volumesExcellent (petabytes)Good (tens of TB)
Lots of small files / heavy metadata churnAverageSlightly better
Shrink (resize smaller)Not supportedSupported
Grow (resize larger)Yes (xfs_growfs)Yes (resize2fs)
SnapshotsThrough LVM (external)Through LVM (external)

The most-quoted difference is no shrinking on XFS. Once you size a volume, you can only grow it. Rarely a problem in practice, but worth knowing.

Other filesystems you’ll seebtrfs was a tech preview on RHEL 7 and officially deprecated from RHEL 8. Don’t use it. Stratis stepped in as the replacement (Intermediate #3). For container workloads you’ll see overlayfs, but that belongs to Docker / Podman’s #4 Volumes and networks.

Looking at disks — lsblk / df / du #

lsblk — block device tree #

block devices
$ lsblk
NAME            MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
vda             252:0    0   40G  0 disk
├─vda1          252:1    0    1G  0 part /boot
└─vda2          252:2    0   39G  0 part
  ├─rhel-root   253:0    0   25G  0 lvm  /
  ├─rhel-swap   253:1    0    4G  0 lvm  [SWAP]
  └─rhel-home   253:2    0   10G  0 lvm  /home
vdb             252:16   0   20G  0 disk                ← newly attached disk

vda is the system disk; vdb is an empty disk we attached for later. In virtualized environments names like vda / vdb are common; on bare metal or older virtualization, sda / sdb show up more.

df — usage of mounted filesystems #

df
$ df -hT
Filesystem                Type   Size  Used Avail Use% Mounted on
/dev/mapper/rhel-root     xfs     25G   12G   13G  48% /
/dev/vda1                 xfs    974M  254M  720M  27% /boot
/dev/mapper/rhel-home     xfs     10G  1.2G  8.8G  13% /home
tmpfs                     tmpfs  1.8G   12M  1.8G   1% /run

-h is human-readable units, -T shows filesystem types. When you get a disk-usage alert in production, this is usually the first command.

du — usage by directory #

common du patterns
$ du -sh /var/log                           # total of /var/log only
$ du -sh /var/* | sort -h                   # each subdirectory under /var, sorted
$ du -sh /var/log/* | sort -h | tail -10    # 10 biggest

When / fills up, the typical flow is du -sh /* and then drilling into the heavy directory.

Adding a new disk #

Now the hands-on part. Take the /dev/vdb we saw with lsblk and mount it whole at /data.

Adding a disk to your VM — UTM: stop the VM, then Edit → New → add a Virtual Drive. VirtualBox: VM Settings → Storage → Controller: SATA → Add Hard Disk.

1) Confirm the disk is recognized #

is the new disk visible?
$ lsblk
NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINTS
vda    252:0    0   40G  0 disk
...
vdb    252:16   0   20G  0 disk         ← empty disk

2) Create a partition — parted or gdisk #

A small disk can stay as one big partition. Disks ≥ 2TB require GPT. parted handles both GPT and MBR cleanly.

one GPT partition with parted
$ sudo parted /dev/vdb mklabel gpt
$ sudo parted /dev/vdb mkpart data xfs 0% 100%
$ sudo parted /dev/vdb print
Model: Virtio Block Device (virtblk)
Disk /dev/vdb: 21.5GB
Partition Table: gpt
Disk Flags:

Number  Start   End     Size    File system  Name  Flags
 1      1049kB  21.5GB  21.5GB               data

A new partition /dev/vdb1 is created (vdb + 1).

verify
$ lsblk /dev/vdb
NAME    MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
vdb     252:16   0  20G  0 disk
└─vdb1  252:17   0  20G  0 part

You can also format the whole disk without a partition (mkfs.xfs /dev/vdb directly). The recommended path is to keep a partition table — other tools recognize it more cleanly.

3) Make a filesystem — mkfs.xfs #

format as XFS
$ sudo mkfs.xfs /dev/vdb1
meta-data=/dev/vdb1     isize=512    agcount=4, agsize=...
data     =              bsize=4096   blocks=...
naming   =version 2     bsize=4096   ascii-ci=0, ftype=1
log      =internal log  bsize=4096   blocks=...
realtime =none          extsz=4096   blocks=0, rtextents=0
Discarding blocks...Done.

For ext4: sudo mkfs.ext4 /dev/vdb1. Different output, same flow.

4) Make a mount point and temp-mount #

mount
$ sudo mkdir -p /data
$ sudo mount /dev/vdb1 /data
$ df -hT /data
Filesystem     Type  Size  Used Avail Use% Mounted on
/dev/vdb1      xfs    20G  175M   20G   1% /data

/data works at this point — but it disappears after reboot. That’s a temporary mount. The next step makes it permanent.

5) Permanent registration in /etc/fstab — by UUID #

/etc/fstab is the list of filesystems to auto-mount on boot.

check the UUID
$ sudo blkid /dev/vdb1
/dev/vdb1: UUID="abc12345-67de-89fa-bcde-1234567890ab" TYPE="xfs" PARTUUID="..."

Copy the UUID and add a line to /etc/fstab:

/etc/fstab — append
UUID=abc12345-67de-89fa-bcde-1234567890ab  /data  xfs  defaults  0  0

Six columns:

#Meaning
1Device — UUID is preferred (device names can change between boots)
2Mount point
3Filesystem type
4Options — defaults, noatime, nofail, etc.
5dump backup flag (almost always 0)
6fsck order — / is 1, others 2 or 0

6) Verify with mount -a #

A typo in /etc/fstab can block boot, so always verify before rebooting.

verify
$ sudo umount /data                  # drop the temp mount
$ sudo mount -a                      # try every entry in /etc/fstab
$ df -hT /data
/dev/vdb1      xfs    20G  175M   20G   1% /data        ← back via /etc/fstab

If mount -a finishes without errors, you’re safe. If something errors out, fix the line and run again.

Watch out — booting with a broken /etc/fstab halts at the mount stage and drops you into emergency mode (emergency.target). On RHEL 9, adding nofail lets boot continue even if a particular entry fails — recommended on data disks.

safer line
UUID=abc12345-...  /data  xfs  defaults,nofail  0  0

Common /etc/fstab options #

OptionMeaning
defaultsrw, suid, dev, exec, auto, nouser, async (default bundle)
noatimeDon’t update access time on read — faster, recommended
nofailContinue boot if this entry fails (use on data disks)
ro / rwRead-only / read-write
noexecBlock executing binaries here — security on /tmp, etc.
nosuidIgnore SUID bits — security
nodevIgnore device files — security
_netdevNetwork filesystem (NFS, iSCSI) — wait for network

For data disks in production, the standard is defaults,noatime,nofail. Add nosuid,nodev,noexec when security matters.

swap #

Current swap #

swap status
$ swapon --show
NAME                TYPE      SIZE   USED PRIO
/dev/mapper/rhel-swap  partition  4G   12M   -2

$ free -h
               total        used        free     ...
Mem:           3.7Gi       1.2Gi       1.8Gi
Swap:          4.0Gi        12Mi       4.0Gi

Adding a swap file #

When there’s no partition space, you can use a file as swap. Common for temporary or extra swap.

create a 1GB swap file
$ sudo dd if=/dev/zero of=/swapfile bs=1M count=1024 status=progress
$ sudo chmod 600 /swapfile
$ sudo mkswap /swapfile
$ sudo swapon /swapfile
$ swapon --show
NAME       TYPE  SIZE   USED PRIO
...
/swapfile  file  1024M  0B    -3

To make it permanent, add to /etc/fstab:

/etc/fstab
/swapfile  none  swap  sw  0  0

swappiness #

How aggressively the kernel uses swap instead of RAM, on a scale of 0–100. Default is 60.

check / change
$ cat /proc/sys/vm/swappiness
60

$ sudo sysctl vm.swappiness=10                       # immediate
$ echo 'vm.swappiness=10' | sudo tee /etc/sysctl.d/99-swap.conf  # persistent

Servers usually set it lower (10–30). Heavy swap usage tends to slow response times noticeably.

A glance at LVM #

The auto-partitioning from #2 resulted in LVM (/dev/mapper/rhel-root). LVM is an abstraction layer between physical disks and the filesystem.

LVM shape
   PV (Physical Volume)  ─┐
   PV ────────────────────┤   →  VG (Volume Group)  →  LV (Logical Volume)  →  filesystem
   PV ────────────────────┘

The three layers:

  • PV — mark a disk or partition for LVM use
  • VG — a pool of PVs
  • LV — a logical volume carved from a VG. The filesystem sits here

LVM’s real value is growing, shrinking, and snapshots. If /home fills up, add a PV to the same VG and grow the LV. The deep dive lives in Intermediate #2 LVM.

The /dev/vdb1 we created here was a plain partition for simplicity, but in production putting filesystems on LVM is essentially the standard.

Common pitfalls #

“After reboot, the disk isn’t mounted” #

You forgot to register it in /etc/fstab. The bare mount command only affects the running system and is lost on reboot. Add a line to /etc/fstab and verify with mount -a — that’s the answer.

“I broke /etc/fstab and the system won’t boot” #

Boot rescue mode from the GRUB menu and edit /etc/fstab directly. As prevention, add nofail. And always run mount -a before rebooting a production box.

“The device name changed” #

/dev/sda becomes /dev/sdb after a reboot. SATA slot changes, USB ordering, or how disks were attached in virtualization. That’s why UUID is the recommended wayblkid, then /etc/fstab.

“df and du don’t agree” #

df reads the filesystem metadata; du walks the actual files. If a process still holds a deleted file open, df counts it as used while du doesn’t see it. lsof | grep deleted finds those processes — restarting them frees the space.

“I want to shrink XFS” #

You can’t. The path is back up the data → make a smaller filesystem → restore. Even on LVM, the LV can shrink, but XFS inside it can’t.

Common commands at a glance #

CommandWhat it does
lsblk [-f]Block device tree (+ filesystem / UUID)
blkid <device>UUID and filesystem type
findmnt [-t xfs]Mount tree
df -hT [path]Filesystem usage
du -sh <path> / du -sh /path/* | sort -hDirectory usage
parted /dev/X mklabel gpt / mkpart NAME xfs 0% 100%Partitioning
mkfs.xfs /dev/X / mkfs.ext4 /dev/XFormat
mount /dev/X /mnt / umount /mntTemporary mount
mount -aTry every /etc/fstab entry
swapon --show / swapoff /pathswap status / off
mkswap / swaponCreate and enable swap
xfs_growfs /mnt / resize2fs /dev/XGrow (after extending the LV)

Wrap-up #

The picture from this post:

  • The default fs on RHEL 9 is XFS — strong on big volumes, but can’t shrink.
  • lsblk (block tree) / df (mounted fs usage) / du (per directory) — the disk-diagnosis trio.
  • New disk flow: recognize → partedmkfs.xfsmount → register in /etc/fstab → verify with mount -a.
  • Register /etc/fstab entries by UUID (device names can change). Use nofail for data disks.
  • Swap as a partition or a file. swappiness is usually 10–30 on servers.
  • In production, put filesystems on LVM — grow / snapshots are standard (Intermediate #2).

Next — basic security #

With the filesystem in place, data lives on top of it, and the paths external traffic takes are the firewall and SSH. The last post and the convergence point of the series.

#7 Basic security — firewalld, SSH hardening covers RHEL’s firewall abstraction firewalld and its zone model, the firewall-cmd family (permanent vs runtime), common zone/service/port patterns, and SSH hardening — disabling password auth, key-only auth, locking down root login, port changes, splitting sshd_config.d/ — all in one final post.

X