Add a block storage volume to your VPS
Attach a second disk to a Linux VPS: find the device with lsblk, make a filesystem, mount it, and persist it in /etc/fstab by UUID with nofail.
What adding a block storage volume to a VPS involves
Adding a block storage volume to a VPS is four steps: find the device the platform attached, put a filesystem on it, mount it at a path you choose, and write it into /etc/fstab so it survives a reboot. The commands take a few minutes. Most of the trouble lives in the last two steps, because a mount point with the wrong ownership stops a service from writing, and a wrong /etc/fstab line can stop the server from booting at all.
A block storage volume is a virtual disk the platform attaches to your server over its own network. The kernel sees an ordinary block device, the same way it sees the root disk, and it arrives raw: no partition table, no filesystem, no mount point. Everything below turns that raw device into a directory your software can use, and makes that directory come back on its own after every reboot.
Two words are worth fixing before the commands. A filesystem is the structure written onto the device that lets it hold files. A mount point is the directory where that filesystem appears in your tree. Formatting and mounting are separate acts, and each one fails in its own way.
Find the device instead of guessing /dev/sdb
Attach the volume in your provider's panel, then look at the server. lsblk lists every block device the kernel knows about, and lsblk -f adds the filesystem type, label and UUID (universally unique identifier) of each one.
lsblk
lsblk -fThe disk carrying / is your root disk. The new volume is the device with no filesystem type, no mount point, and a size matching what you ordered. Do not copy a device name out of a tutorial. Names come from the order the kernel finds the disks, so they are a description of this boot, not a promise. Detach and reattach the volume, or add a second one, and a name can belong to a different disk after the next reboot. That is why every persistent reference below uses a UUID rather than a name.
Many platforms also expose the volume under /dev/disk/by-id/ with the name you typed in the panel, which settles it when two devices look alike.
ls -l /dev/disk/by-id/Set the device once as a shell variable so nothing below hard-codes a name that is not yours. Replace the value with what lsblk showed you.
DEV=/dev/vdb
lsblk -f "$DEV"
sudo wipefs -n "$DEV"wipefs -n reports what it would erase and erases nothing. On a fresh volume it prints no lines. If it prints a signature, that device already holds a filesystem or a partition table, so stop and work out which disk you are looking at before you format anything. On some platforms the volume appears with an NVMe style name instead, which changes nothing here once the name is in $DEV. What the device is underneath does change what you should expect from it, and checking whether a disk is real NVMe or a network volume takes one command.
Partition the volume, or format the whole device?
Both work. A filesystem written straight onto the whole device is valid, and so is a single partition that fills the device.
The whole device is simpler to grow. After the platform enlarges the volume, one command takes the filesystem to the new size. With a partition you grow the partition first, then the filesystem inside it: two commands and two chances to get it wrong.
A partition table is worth having when something else has to read the disk, or when a person may attach it to a machine that expects one. Some tools read a bare filesystem on a whole disk as an uninitialised disk and offer to set it up, which is a bad thing to click on. If you want the partition, create one that fills the device.
sudo apt update && sudo apt install -y gdisk
sudo sgdisk --new=1:0:0 --typecode=1:8300 "$DEV"
sudo partprobe "$DEV"
lsblk "$DEV"
PART=/dev/vdb1Read the partition name out of that lsblk output rather than assuming it. A partition on a virtio disk appends 1 to the disk name, while a partition on an NVMe disk appends p1. If you took this path, use $PART everywhere the rest of this guide uses $DEV, because the filesystem goes on the partition and not on the disk.
Make a filesystem
mkfs destroys whatever is on the device and it does not ask twice. Run the wipefs -n check one more time before this line.
sudo mkfs.ext4 -m 1 -L data "$DEV"-L data labels the filesystem, so lsblk -f names it back to you later. -m 1 is the flag worth explaining. ext4 reserves five percent of the filesystem for the root user by default, so a full disk still leaves room for system processes to write and for you to log in and fix it. That protection earns its keep on a root filesystem. On a data volume nothing needs it, and five percent of a large volume is space you pay for every month and can never use, so one percent is enough.
For XFS, which is the default on the Red Hat family:
sudo mkfs.xfs -L data "$DEV"Both grow while mounted. The difference that matters later is shrinking: ext4 shrinks only while unmounted, and XFS cannot shrink at all. Block volumes usually grow and rarely shrink, so this is a small point, but choose knowing it.
Mount it and prove it works
sudo mkdir -p /mnt/data
sudo mount "$DEV" /mnt/data
findmnt /mnt/data
df -h /mnt/datafindmnt prints one line describing the mount and exits 0. When the mount is absent it prints nothing and exits 1, which makes it the right command to put in a script. df -h reports slightly less than the size you ordered, because filesystem metadata takes its cut. Then write something and read it back, because a mount can succeed on a device that is broken further down.
sudo touch /mnt/data/write-test && sudo rm /mnt/data/write-testMount over an empty directory. If /mnt/data already holds files, mounting hides them for as long as the volume is mounted, and they come back when you unmount. Hidden files still occupy space on the root disk, which is one of the reasons df and du disagree about how full a disk is.
Persist the mount in /etc/fstab by UUID with nofail
The /etc/fstab entry is what makes the mount survive a reboot. Two parts of the line matter more than the rest: the filesystem is named by UUID, and the options include nofail.
VOL_UUID=$(sudo blkid -s UUID -o value "$DEV")
echo "$VOL_UUID"
echo "UUID=$VOL_UUID /mnt/data ext4 defaults,nofail,x-systemd.device-timeout=10s 0 2" | sudo tee -a /etc/fstabRead that line field by field. UUID= names the filesystem by an identifier written inside it at mkfs time, so the entry follows the data even when the kernel calls the device something new. /mnt/data is the mount point. ext4 is the type, or xfs if you made XFS. After the options comes 0 for dump, which nothing uses today, then the filesystem check pass: 2 for a non-root ext4 filesystem, and 0 for XFS, which has no boot time check.
nofail is the option that keeps you out of a recovery console. Without it, systemd treats the mount as required by local-fs.target. If the volume is detached, deleted, or slow to appear, that target fails and the boot stops in emergency mode, asking for a root password on a console you may not have. With nofail a missing volume means the server boots without that one directory, and you fix it over SSH. x-systemd.device-timeout=10s bounds how long systemd waits for the device to show up instead of sitting through the ninety second default.
Test the line before you trust it.
sudo findmnt --verify --verbose
sudo umount /mnt/data
sudo systemctl daemon-reload
sudo mount -a
findmnt /mnt/datafindmnt --verify parses /etc/fstab and reports problems such as a mount point that does not exist or a filesystem type the kernel does not know. mount -a mounts everything in the file that is not mounted yet, so it catches a wrong UUID or a wrong type. daemon-reload regenerates the systemd mount units from the file, which is what actually runs at boot.
Then reboot once, now, while the volume is still empty, and run findmnt /mnt/data again. That is the only proof that counts. A minute of downtime you chose is cheap. The same discovery during an incident is not.
Move a data directory onto the volume without breaking permissions
This is why most people add a volume: a database, a container's data, or a mail store has outgrown the root disk. The move is mechanical, and the two traps in it are both about permissions.
- Stop the service that owns the data, so nothing writes while you copy.
- Mount the volume at
/mnt/dataand make a subdirectory for this service. - Copy with
rsync -aHAX, which preserves ownership, permissions, timestamps, hard links, ACLs (access control lists) and extended attributes. - Verify the copy with a dry run, fix ownership on the new directory, then point the service at the new path.
- Start the service, confirm it writes to the new path, and keep the old directory until you are sure.
sudo apt install -y rsync
sudo systemctl stop postgresql
sudo mkdir -p /mnt/data/postgresql
sudo rsync -aHAX /var/lib/postgresql/ /mnt/data/postgresql/
sudo rsync -aHAXn --delete --itemize-changes /var/lib/postgresql/ /mnt/data/postgresql/The trailing slash on the source is not decoration. Without it, rsync copies the directory into the target and everything lands one level deeper than you meant. The second command repeats the copy as a dry run: -n changes nothing and --itemize-changes lists every difference it would still fix. No output means the two trees match.
Trap one: copying before mounting. Copy into /mnt/data while the volume is not mounted and the files land on the root disk, inside the directory the volume will later cover. The copy looks fine, the mount looks fine, and the data is invisible. Run findmnt /mnt/data before you copy anything.
Trap two: the root of a fresh filesystem belongs to root. mkfs creates the top directory as root:root with mode 755, so a service running as its own user cannot create anything there. Fix the subdirectory rather than the mount point, and copy the ownership from the old location instead of typing a guess. stat prints exactly what to set, and chown changes the owner and the group together.
stat -c '%U:%G %a' /var/lib/postgresql
sudo chown -R postgres:postgres /mnt/data/postgresql
sudo chmod 700 /mnt/data/postgresqlAn ext4 filesystem also carries a lost+found directory at its root, and some software refuses to start on a data directory that is not empty. PostgreSQL is one. That alone is a good reason to mount the volume at /mnt/data and give every service a subdirectory under it, rather than mounting the volume straight onto the service's data path. PostgreSQL then needs data_directory in its config file changed to the new path.
On Fedora, Rocky or Alma the new path also needs an SELinux (security enhanced Linux) file context, or the service is refused access to files it owns. The refusal appears in ausearch -m avc -ts recent rather than in the service log, which is what makes it confusing. semanage fcontext -a -t <type> '/mnt/data/postgresql(/.*)?' followed by restorecon -Rv /mnt/data/postgresql sets it, and semanage comes from the policycoreutils-python-utils package on those distributions. Working with SELinux contexts on a server covers how to find the right type instead of switching SELinux off.
Docker has its own route in. Do not rsync /var/lib/docker with the daemon running. The supported move is data-root in /etc/docker/daemon.json, pointing at a directory on the volume, then a copy of the old tree with both docker and containerd stopped, then a daemon restart.
{
"data-root": "/mnt/data/docker"
}If you only want one application's data on the volume, a bind mount in the compose file is far less work, and the difference between bind mounts and named volumes decides which of the two jobs you are doing.
When a service must keep seeing its data at the original path, bind mount the new directory over the old one instead of editing config. Add a second line to /etc/fstab:
/mnt/data/postgresql /var/lib/postgresql none bind,nofail 0 0systemd sees that the bind source lives under /mnt/data and mounts the volume first, so ordering takes care of itself. The service is a different matter. A unit that starts before the mount writes into the empty directory underneath it, so add a drop-in with sudo systemctl edit postgresql:
[Unit]
RequiresMountsFor=/mnt/dataKeep backups on a volume, and know what that does not cover
The other common use for a volume is somewhere to put backups. It is good for that, with one honest limit: the volume is attached to the same server, so anything with root on that server can erase it. A mistyped rm -rf, a bad deploy script, or an intruder reaches a mounted filesystem as easily as the root disk. Copies that live only on an attached volume protect you against a failed disk, not against a mistake.
Two things improve that. Mount the backup volume with options that limit what can run from it: nosuid, nodev and noexec on a data mount cost nothing and stop the backup directory from being a convenient place to execute code. Then keep a second copy somewhere this server cannot reach with a delete.
UUID=<your uuid> /mnt/backup ext4 defaults,nofail,nosuid,nodev,noexec 0 2Check what your panel's snapshot really covers before you rely on it. On several platforms a server snapshot takes the boot disk only, and an attached volume is snapshotted as a separate object with its own schedule and its own charge. Assume nothing, because a snapshot and a backup answer different questions and the gap between them is usually found during a restore. If the off-box copy is the goal, weigh block storage against object storage per terabyte before buying a second volume.
Run the copy on a schedule and make it visible. A systemd service with a timer is enough, and it gives you systemctl list-timers plus one journal entry per run. Then restore a single file into a temporary directory today. A backup nobody has restored from is a belief, not a backup.
What a block storage volume costs, and why the region decides your options
European hosts sell block storage as its own line item, billed per gigabyte-month on the size you provisioned rather than the space you filled. A volume you detach keeps billing until you delete it, which is the usual surprise on an invoice after a migration. As of September 2026 that model is close to universal, so size the volume with an honest estimate of the storage you need instead of rounding up because the monthly number looks small.
A volume belongs to a region, often to one zone inside that region, and it attaches only to a server in the same place. It follows the region, not the server. Moving your workload to another region means creating a new volume there and copying the data across the network, so a region change and a storage change are one decision rather than two. Most platforms also allow one attached server at a time, so a volume is not shared storage for several machines.
Growth runs one way in practice. Volumes grow online with no downtime. Shrinking is either unsupported or a full copy onto a smaller volume, and XFS cannot shrink at all. Start at a size you are content to pay for, and grow when the filesystem passes about eighty percent full.
Grow the filesystem after the platform resizes the volume
Enlarging the volume in the panel changes the device. It does not change the filesystem, so df keeps reporting the old size until you grow the filesystem on top.
lsblk "$DEV"
sudo resize2fs "$DEV"
df -h /mnt/dataext4 grows while mounted. For XFS the command takes the mount point rather than the device: sudo xfs_growfs /mnt/data. If you partitioned the volume, grow the partition first, then the filesystem inside it. growpart comes from cloud-guest-utils on Debian and Ubuntu, and from cloud-utils-growpart on Fedora and RHEL.
sudo apt install -y cloud-guest-utils
sudo growpart "$DEV" 1
sudo resize2fs "$PART"If lsblk still reports the old size after the platform says the resize is done, the kernel has not noticed the change yet. A reboot always settles it.
Failure modes, and the message you will see
The server does not come back after a reboot. The console shows You are in emergency mode, and the journal has Dependency failed for Local File Systems above a failed mount. An /etc/fstab entry without nofail, pointing at a device that is not present, is the usual cause: a detached volume, a deleted volume, or a UUID that never matched. Open the recovery console your platform provides, comment out the line, reboot, then fix it properly.
mount: /mnt/data: special device /dev/vdb does not exist. The name in your command or in /etc/fstab is not what the kernel calls that disk right now. Run lsblk, then use the UUID. This is the exact failure that naming by UUID prevents.
mount: /mnt/data: wrong fs type, bad option, bad superblock. Either the type does not match the filesystem, ext4 written for an XFS volume, or you are pointing at the whole disk while the filesystem sits on a partition. lsblk -f shows the type the kernel found on every device.
umount: /mnt/data: target is busy. A process holds a file open under that path, or a shell has its working directory there. sudo fuser -vm /mnt/data lists the processes and sudo lsof +f -- /mnt/data gives more detail. A bind mount of the same filesystem elsewhere also pins it, so check findmnt for a second entry.
The service runs but the volume stays empty. Either the config still names the old path, or the unit started before the mount and wrote into the directory underneath. findmnt /mnt/data confirms the mount, sudo lsof +D /mnt/data shows whether anything is using it, and the RequiresMountsFor= drop-in above fixes the ordering case.
The volume reports full sooner than the files explain. A deleted file that a process still holds open keeps its blocks until that process closes it, so df and du disagree until the writer restarts. The reserved-blocks setting from mkfs.ext4 -m accounts for a smaller, constant gap in the same direction.
FAQ
How do I find the device name of a new block storage volume?
Run lsblk -f on the server after attaching the volume in the panel. The new volume is the device with no filesystem type and no mount point, at the size you ordered. ls -l /dev/disk/by-id/ often shows the name you gave the volume, which settles it when two devices look alike. Confirm it is blank with sudo wipefs -n against that device: it prints nothing on a fresh volume and lists signatures on a device that already holds data. After that, stop using the name. Reference the filesystem by UUID from sudo blkid, because device names can change across reboots and reattachments.
Why does my VPS drop to emergency mode after I edit /etc/fstab?
An entry without nofail is required by local-fs.target. When the device is missing, that target fails, and systemd stops the boot with You are in emergency mode and asks for the root password on the console. A detached volume, a deleted volume, or a mistyped UUID all produce it. Add nofail to every entry for an attached volume so the server boots without the mount instead. Before rebooting, run sudo findmnt --verify --verbose and sudo mount -a, which catch a bad type or a wrong UUID while you still have a shell.
Should I partition a block storage volume or format the whole device?
Both are valid. Formatting the whole device is simpler to grow later, because one resize2fs or xfs_growfs call finishes the job after the platform enlarges the volume. A partition needs growpart first and then the filesystem command, so there are two steps to get right. Create a partition when other tooling expects a partition table, or when the disk may be attached to a machine that offers to initialise anything it does not recognise.
Why can my service not write to the new volume?
A fresh filesystem has a root directory owned by root:root with mode 755, so a service running under its own user cannot create files at the top of the mount. Give the service a subdirectory and set the ownership to match the old location, which stat -c '%U:%G %a' on the old path prints for you. On Fedora, Rocky and Alma also set an SELinux file context for the new path, since the refusal is logged by the audit system through ausearch -m avc -ts recent and not by the service. If the directory looks right but the data is missing, check with findmnt that the volume was mounted before you copied, and not after.
Is a block storage volume a backup of my server?
No. The volume is attached to the running server, so any process with root can delete what is on it, the same as on the root disk. It protects you against the boot disk filling up and gives you a place to write copies, which is worth having. A backup needs a copy the server cannot reach and delete, plus a restore you have actually tested. Check separately whether your platform's server snapshot includes attached volumes, because on many platforms a volume is snapshotted as its own object with its own schedule and its own charge.