Bare Metal Dedicated Server ZFS Migration on AlmaLinux 9

Why this migration matters on a bare metal dedicated server
If your dedicated server hosting setup has outgrown simple ext4 partitions, ZFS gives you cleaner snapshots, faster rollbacks, and better control over data-heavy workloads. On a bare metal dedicated server, that matters when you run databases, customer files, or staging environments that need a predictable recovery path.
This tutorial walks through a production-safe bare metal dedicated server ZFS migration on AlmaLinux 9. You will boot a fresh server, prepare a non-root admin, install ZFS from the supported repository path, create a mirrored ZFS layout, move application data into it, make the pool persistent at boot, and verify recovery paths. The procedure is written for a new Hostperl bare metal server or a rebuilt system after a storage refresh.
If you are comparing storage options before you move, Hostperl’s dedicated servers and bare metal dedicated server hosting pages are a good starting point. If you are planning a storage migration as part of a broader rebuild, also review Dedicated Server ZFS Backup Rotation on openSUSE Leap for snapshot discipline and Linux package update safety practices that still apply to production servers.
What you need before you start
- AlmaLinux 9 on a bare metal dedicated server
- Root SSH access for the first login
- A second terminal for testing the non-root account
- At least two disks if you want a mirrored ZFS pool
- One maintenance window if this is a live migration
This tutorial assumes a clean installation or a server that can be placed into maintenance mode. If you are migrating a live workload, stop the application first and take a backup before touching storage. ZFS is forgiving, but storage changes are not something to improvise on a production server.
Connect and identify the operating system
On your local computer, start with the initial SSH login. Replace the example IP with the public address assigned to your own server.
ssh root@203.0.113.10203.0.113.10 is a reserved documentation address. Replace it with the real public IP from your hosting provider. Keep this root session open until the new admin login is confirmed.
On the VPS as root, confirm the operating system before you run distribution-specific commands.
cat /etc/os-releaseYou should see AlmaLinux 9 details. If the server is not AlmaLinux 9, stop here and use the correct distribution branch. This guide is written for AlmaLinux and RHEL-compatible systems because ZFS deployment on a bare metal dedicated server is most predictable there when using the supported ZFS repository path.
Create a sudo user and lock down SSH access later
Run this before you change SSH policy. You need a non-root account ready and tested first.
On the VPS as root, create the admin user, set its password, and add it to the wheel group.
useradd -m -s /bin/bash deploy
passwd deploy
usermod -aG wheel deploydeploy is the example non-root admin name used throughout this tutorial. The password prompt appears after passwd. You should see no errors from usermod.
Now create SSH key access for that account. If you already have a public key on your local computer, copy it in the safest simple way from a second terminal.
On your local computer, open a new terminal and run:
ssh-copy-id deploy@203.0.113.10Replace 203.0.113.10 with your real server IP. If ssh-copy-id is unavailable, paste the public key manually after creating ~deploy/.ssh/authorized_keys on the server.
On the VPS as root, if you need to create the directory manually, run:
mkdir -p /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chown deploy:deploy /home/deploy/.sshThen place the public key in /home/deploy/.ssh/authorized_keys, set ownership, and lock permissions:
chmod 600 /home/deploy/.ssh/authorized_keys
chown deploy:deploy /home/deploy/.ssh/authorized_keysOn your local computer, test the new login in a second terminal and confirm sudo works.
ssh deploy@203.0.113.10
sudo -vYou should be prompted for the deploy password the first time, then for your sudo password check. Do not disable root login yet. Keep the original root session open until this succeeds.
Update the server and install ZFS support
On AlmaLinux 9, use the supported ZFS repository path rather than random third-party packages. This avoids mismatched kernel module builds on bare metal hardware.
On the VPS as root, update the system and install the tools needed for repository management.
dnf -y update
dnf -y install epel-release dnf-plugins-core wget vimRun a reboot later if the kernel changes. For now, continue and add the ZFS repository.
dnf config-manager --add-repo https://zfsonlinux.org/almalinux/zfs.repoNow install the kernel module and user tools.
dnf -y install zfs zfs-dracutConfirm the module package is present.
rpm -qa | grep -E '^zfs|zfs-dracut'If Secure Boot is enabled on your server, unsigned kernel modules may fail to load. On many bare metal dedicated servers, the simplest operational choice is to disable Secure Boot in firmware before attempting ZFS. If that is not possible, you will need a signing workflow.
Load the module and verify kernel support
On the VPS as root, load ZFS and confirm that the kernel module is active.
modprobe zfs
lsmod | grep zfs
zpool versionYou should see zfs in the loaded modules list and a version string from zpool version. If modprobe fails, check journalctl -k -b for module load errors before continuing.
Create the ZFS pool for production storage
This example uses a mirror because most production bare metal dedicated server deployments value uptime more than raw capacity. Adjust the device names to match your disks.
On the VPS as root, identify disks carefully.
lsblk -o NAME,SIZE,TYPE,MODEL,FSTYPE,MOUNTPOINTLook for the empty data disks and confirm the boot disk is not included by mistake. Then create the pool. This command erases the selected disks, so do not copy it blindly onto a server with existing data.
zpool create -o ashift=12 tank mirror /dev/sdb /dev/sdcashift=12 suits 4K-sector SSDs and NVMe-backed storage on modern bare metal hardware. If your server uses different device names, replace them with the correct disk identifiers from lsblk or /dev/disk/by-id/.
Check the pool health immediately.
zpool status tank
zpool listYou want the pool to show ONLINE with both devices in the mirror. If one side is missing, stop and fix cabling or the device path before you build on top of it.
Create datasets for applications and backups
ZFS works best when you split data into datasets instead of dumping everything into one mount point. For a dedicated server, this makes application restores and quota planning much easier.
On the VPS as root, create a small dataset structure.
zfs create tank/appdata
zfs create tank/appdata/web
zfs create tank/backupsSet useful properties for server workloads.
zfs set compression=lz4 tank
zfs set atime=off tank
zfs set recordsize=16K tank/appdata/webCheck the resulting layout.
zfs list
zfs get compression,atime,recordsize tank tank/appdata/webCompression usually reduces write volume without adding operational complexity. Turning off atime cuts unnecessary metadata writes. A smaller record size helps many web and database workloads, though you should change it based on the actual application profile.
Move application data safely onto ZFS
Suppose the current web content lives in /var/www. Stop the application first so files do not change mid-copy.
On the VPS as root, stop the service you are migrating. Replace httpd, mariadb, or another service name with the one you actually use.
systemctl stop httpdNow copy data into the new dataset with archive-preserving flags.
rsync -aHAX --delete /var/www/ /tank/appdata/web/After the copy, compare the source and destination sizes.
du -sh /var/www /tank/appdata/webIf the destination matches closely and the application is quiet, switch the mount path. One safe approach is to back up the original directory and bind the new path in place.
mv /var/www /var/www.old
mkdir -p /var/www
mount --bind /tank/appdata/web /var/wwwTest the content and permissions.
ls -la /var/wwwIf the site or app expects a different ownership model, fix it now with chown before the service restart. Restore points are still easy because the original data remains in /var/www.old until you remove it later.
Make the ZFS mount persistent at boot
For production bare metal use, the pool must import cleanly after a reboot. Enable the service and verify the import cache.
On the VPS as root, export and re-import only if you need to generate clean cache data. For a live server, keep the pool online and just enable the service.
systemctl enable zfs-import-cache zfs-mount zfs.targetConfirm the service state.
systemctl status zfs-import-cache zfs-mount zfs.targetNow make sure the pool name appears in the cache file.
cat /etc/zfs/zpool.cacheOn some systems the cache file is binary and not human-friendly. That is fine. The important part is that the ZFS import service is enabled and the pool imports on boot.
Open the firewall and keep management access safe
AlmaLinux uses firewalld by default. Keep SSH open before you apply stricter rules. If your server already accepts only management traffic, verify that first.
On the VPS as root, check the active firewall state and allow SSH if needed.
systemctl status firewalld
firewall-cmd --get-active-zones
firewall-cmd --permanent --add-service=ssh
firewall-cmd --reload
firewall-cmd --list-servicesIf you host web services on this bare metal dedicated server, add the needed ports before you restart the application.
firewall-cmd --permanent --add-service=http
firewall-cmd --permanent --add-service=https
firewall-cmd --reloadThat sequence keeps you from locking yourself out. Never remove SSH access until the new login path is proven from a second terminal.
Check SELinux before service startup
On AlmaLinux, SELinux often helps rather than hurts, but it can block a new mount path if labels are wrong. Start by checking status.
On the VPS as root, inspect SELinux mode.
getenforce
sestatusIf you moved a web root to /var/www, label it normally first. If the application uses a custom path, apply a web-safe label only where required.
restorecon -Rv /var/wwwIf you must troubleshoot a denial, inspect audit logs.
ausearch -m AVC -ts recent
journalctl -t setroubleshoot --since "1 hour ago"Do not disable SELinux just to make a mount path work. Adjust labels or policy instead. That is the safer choice on a managed or self-managed dedicated server.
Reboot and test persistence
Now that the pool, mount, firewall, and service setup are in place, reboot once. This is the most useful test of the whole migration.
On the VPS as root, reboot the server.
rebootWait for the server to return, then reconnect.
On your local computer, log in with the non-root account.
ssh deploy@203.0.113.10After login, check that the pool imported and the mount is present.
On the VPS as the non-root sudo user, run:
sudo zpool status tank
sudo zfs list
findmnt /var/wwwYou want the pool online and the bind mount or ZFS dataset mounted where the application expects it. If the mount is missing, inspect systemctl status zfs-import-cache zfs-mount and the boot logs.
Bring the application back and smoke test it
Start the service again after storage is verified.
On the VPS as the non-root sudo user, restart your app service.
sudo systemctl start httpd
sudo systemctl enable httpd
sudo systemctl status httpdThen test from the server itself and from your local machine.
On the VPS as the non-root sudo user, check the local listener.
sudo ss -tulpn | grep -E ':80|:443'On your local computer, request the site or health endpoint.
curl -I http://203.0.113.10You should receive a valid HTTP response. If you are using HTTPS, test that too after certificates are in place. For this migration tutorial, the storage move is the core task; TLS can be added once the application is stable.
Rollback if the new storage path fails
If the app fails after the cutover, you still have the original data and can revert quickly. That is why the old directory was kept instead of deleted immediately.
On the VPS as the non-root sudo user, stop the service and restore the original directory layout.
sudo systemctl stop httpd
sudo umount /var/www
sudo mv /var/www.old /var/www
sudo systemctl start httpdConfirm the service and storage state again.
sudo systemctl status httpd
sudo findmnt /var/wwwIf the failure is ZFS-related, inspect the pool and kernel logs before you retry.
sudo zpool status -v tank
sudo journalctl -b | grep -i zfsA clean rollback is part of the design. On a bare metal dedicated server, that is better than forcing a half-working mount into production.
Troubleshooting the most likely failures
1) The ZFS module does not load
Diagnostic:
modprobe zfs
journalctl -k -b | grep -i zfsExpected clue: missing symbols, Secure Boot denial, or a kernel-module mismatch.
Next action: install the matching ZFS package for your current kernel, or reboot into the kernel that matches the installed module. If Secure Boot blocks unsigned modules, disable Secure Boot or use signed modules.
2) The pool is degraded after reboot
Diagnostic:
zpool status -v tank
ls -l /dev/disk/by-id/Expected clue: one device appears under a different name or does not appear at all.
Next action: switch to stable /dev/disk/by-id paths for future recreation or replace the missing disk and resilver the mirror.
3) The application cannot read the new path
Diagnostic:
ls -ld /var/www
getenforce
ausearch -m AVC -ts recentExpected clue: wrong ownership, SELinux denial, or a missing mount.
Next action: run chown for the app user, restore SELinux labels with restorecon, and confirm the bind mount or dataset is present.
4) SSH is reachable from one terminal but not another
Diagnostic:
firewall-cmd --list-all
ss -tulpn | grep sshdExpected clue: firewall rules changed before the safe test was done, or sshd is not listening on the expected port.
Next action: re-add the SSH service to firewalld, then check /etc/ssh/sshd_config before changing anything else.
Why this layout works for production
A mirrored ZFS pool is not the only valid layout, but it is a practical one for many bare metal dedicated server builds. It gives you snapshot-friendly storage, quick rollback options, and clearer backup workflows than a single oversized filesystem. On Hostperl infrastructure, that is useful for agencies, ecommerce teams, and operators who want the control of dedicated hardware without adding unnecessary storage risk.
If you are planning the same move on a fresh server, consider pairing this storage layout with a dedicated server plan sized for your workload and recovery objectives. Hostperl’s dedicated server hosting and unmanaged dedicated server options fit teams that want direct control, while self-managed dedicated server setups suit operators who are comfortable handling the storage and boot path themselves.
If you are rebuilding a bare metal dedicated server and want storage that is easier to snapshot, roll back, and verify, Hostperl can help you size the machine and plan the migration. Start with dedicated server hosting or unmanaged dedicated server options that match your recovery needs.
For larger workloads, a mirrored ZFS layout is often the better operational choice than a single disk. It gives your support team more room to recover cleanly if an application update or disk failure happens later.
FAQ
Can I use this ZFS migration on Ubuntu or Debian?
Yes, but the repository and package commands differ. AlmaLinux 9 is used here because this tutorial is written for the RHEL-compatible track, which fits the requested operating system rotation.
Should I use RAID and ZFS together?
Usually not for a simple mirrored server. Let ZFS manage the mirror unless you have a specific hardware RAID or controller requirement that your support team understands well.
Can I migrate a live web server without downtime?
You can reduce downtime, but this tutorial assumes a maintenance window for safety. Copy the data while the service is stopped if you want a clean cutover.
What should I back up before the migration?
Back up application data, database dumps, configuration files, and anything under /var/www or your custom path. Test the restore before you commit to the new pool.
How do I know the migration is finished?
The pool is online after reboot, the dataset or bind mount is present, the service starts cleanly, and a client-side HTTP check returns the expected response.
