If you’re running a Debian VPS with Virtualmin and MariaDB, sooner or later you’ll run into the same problem I did: your root partition is almost full, but MariaDB needs even more space to rebuild a huge table.
In my case, I needed to add a generated column and a unique index to an 11 GB InnoDB table. Unfortunately, MariaDB rebuilds the table during this operation, and the remaining free space on my root partition simply wasn’t enough.
Instead of resizing partitions or deleting data, I temporarily moved the MariaDB data directory to a larger secondary volume, completed the database changes, and then moved everything back.
This guide documents the process.
My Server Setup
Here’s the environment I was working with.
- Debian 13
- MariaDB 11.8
- Virtualmin
- A Cloud VPS
- Root filesystem (
/dev/sda1) almost full - Existing 10 GB volume for Virtualmin backups
- New 40 GB volume attached as
/dev/sdc
The important part looked like this:
/dev/sda1 38 GB
Used: 30 GB
Free: 6 GB
/var/lib/mysql
≈15 GB
The largest table was:
bersih.ibd
11 GB
Why Moving the Data Directory Was the Safest Option
Adding a STORED generated column is not just a metadata change.
For large InnoDB tables, MariaDB rebuilds the table.
That means temporary disk usage can easily reach:
- existing table
- rebuilt table
- temporary workspace
- index creation
For an 11 GB table, MariaDB may temporarily need more than 20 GB of free space.
With only 6 GB available, failure was almost guaranteed.
Instead of risking a half-finished rebuild, I decided to temporarily move the entire MariaDB data directory onto the new 40 GB volume.
First, Clean Up Easy Disk Space
Before touching MariaDB, I reclaimed every bit of space I could.
One log file immediately stood out:
/var/log/proftpd/sftp.log
587 MB
Don’t delete log files that services are actively writing to.
Instead, truncate them:
truncate -s 0 /var/log/proftpd/sftp.log
Or:
: > /var/log/proftpd/sftp.log
I also cleaned old system journals:
journalctl --vacuum-time=14d
Then removed cached package downloads:
apt clean
Every extra megabyte helps when you’re working on a nearly full server.
Always Make Two Backups
I know backups are boring until they’re suddenly very exciting.
I created both a logical backup and a physical backup.
Logical backup
mkdir /root/mysql-backup
mysqldump
--single-transaction
--routines
--events
--triggers
--all-databases
> /root/mysql-backup/all.sql
Physical backup
Since the new volume was almost empty, I copied the existing data directory there as an extra safety net.
rsync -aHAX /var/lib/mysql/
/mnt/HC_Volume_555555555/mysql-backup/
Now I had two recovery options if something went wrong.
Moving MariaDB to the New Volume
After stopping MariaDB:
systemctl stop mariadb
I copied the entire data directory:
mkdir /mnt/HC_Volume_555555555/mysql
rsync -aHAX --info=progress2
/var/lib/mysql/
/mnt/HC_Volume_555555555/mysql/
I never delete the original immediately.
Instead, I renamed it:
mv /var/lib/mysql /var/lib/mysql.old
Then created a new mount point:
mkdir /var/lib/mysql
Why I Used a Bind Mount
There are two common ways to move MariaDB’s data directory.
One is editing my.cnf and changing datadir.
The other is using a bind mount.
I chose the bind mount because MariaDB continues believing the data directory is still:
/var/lib/mysql
Nothing in the MariaDB configuration needs to change.
Mount it like this:
mount --bind
/mnt/HC_Volume_555555555/mysql
/var/lib/mysql
Then make it persistent by adding this line to /etc/fstab:
/mnt/HC_Volume_555555555/mysql
/var/lib/mysql
none bind 0 0
After that:
mount -a
Finally, verify permissions:
chown -R mysql:mysql
/mnt/HC_Volume_555555555/mysql
Then start MariaDB again:
systemctl start mariadb
Everything should behave exactly as before.
Running the Table Modification
Originally I planned to run two separate statements.
ALTER TABLE bersih
ADD COLUMN article_url_hash ...
followed by
ALTER TABLE bersih
ADD UNIQUE KEY ...
A better approach is combining them into a single ALTER TABLE.
ALTER TABLE bersih
ADD COLUMN article_url_hash CHAR(32)
GENERATED ALWAYS AS (MD5(article_url)) STORED,
ADD UNIQUE KEY uniq_article_url_hash (article_url_hash);
This often allows MariaDB to rebuild the table only once, reducing both execution time and temporary disk usage.
For an 11 GB table, don’t expect this to finish in a few minutes.
Depending on your storage and CPU, it could easily take an hour or more.
The important part is simple:
Don’t interrupt it.
Moving Everything Back
After the database changes completed successfully, I reversed the process.
Stop MariaDB:
systemctl stop mariadb
Unmount the bind mount:
umount /var/lib/mysql
Remove the temporary bind entry from /etc/fstab.
Then copy everything back:
rsync -aHAX --info=progress2
/mnt/HC_Volume_555555555/mysql/
/var/lib/mysql/
Start MariaDB again:
systemctl start mariadb
Only after verifying that everything worked normally did I remove the old backup directory.
rm -rf /var/lib/mysql.old
Never delete it until you’ve confirmed your databases are healthy.
Common Mistakes to Avoid
A few mistakes can turn a routine migration into a stressful recovery.
- Don’t delete the original data directory before testing the new one.
- Don’t forget to preserve file ownership and permissions.
- Don’t modify MariaDB while it’s still running.
- Don’t run huge
ALTER TABLEoperations without enough temporary disk space. - Don’t skip backups, even if the server has been stable for years.
Most migration problems come from rushing, not from the commands themselves.
Final Thoughts
This ended up being much simpler than resizing partitions or trying to free tens of gigabytes on the root filesystem.
Using a temporary bind mount kept the MariaDB configuration unchanged, made rollback easy, and let me safely complete the table rebuild on a much larger volume.
If you only need the extra space temporarily, this is a clean solution that’s easy to reverse once the database work is finished.
Sometimes the safest fix isn’t changing the application at all—it’s simply giving it enough room to work.
No Comments