A Linux server backup is the only thing standing between you and a truly bad day: hard drives die without warning, one wrong rm wipes out three months of work in three seconds, and ransomware always seems to arrive at midnight. But a backup that has never been restored is just a file, not a guarantee. This guide walks the path step by step: the 3-2-1 rule, an inventory of what lives on your server, a nightly script with tar and cron, MySQL and MariaDB dumps, copying to a second server with rsync, encrypted restic snapshots, and the most important step most admins forget: the restore test.
The 3-2-1 rule in plain English
Every serious backup strategy rests on one simple rule:
- Keep 3 copies of every important piece of data (the original plus two copies).
- Keep the copies on 2 different systems, not in two folders on the same disk.
- Keep 1 copy completely off the main server.
The logic is simple: each layer covers a different failure scenario. A backup on the server itself saves you from accidental deletion and gives the fastest restore, but if the disk dies or ransomware locks the whole server, that backup goes with it. Two different media mean a single shared fault, such as a failed controller or a corrupted file system, can't take out both copies at once, and the offsite copy is for the day you lose the whole machine.
Layer zero of this setup, and the one that takes no effort, is the provider's own backup: an add-on that takes copies at the disk level and is turned on with a single switch. That is how it works in the Mehran Host cloud server panel too, if the add-on is available at your server's location. But don't stop at one layer: that backup lives inside the same infrastructure, and restoring it overwrites the entire disk.
What exactly should you back up?
Most failed backups are not corrupt, they are incomplete: the archive is fine, but a critical folder was never in it. Before you write the first line of a script, write down your inventory:
/etc: all configuration, from the web server, PHP and the firewall to systemd units. It is tiny and worth more than anything else./var/wwwor/home/*/public_html: site files, uploads and.envfiles.- A database dump, not the raw database folder; the next section explains why.
- Users' crontabs, TLS certificates (
/etc/letsencrypt) and~/.ssh/authorized_keys.
It matters just as much to know what should not go into the archive: caches, temporary files, bulky logs and node_modules. They eat space and time without being "data". That is why GNU tar has --exclude=PATTERN, which skips files matching a glob pattern, and --one-file-system, which makes tar "stay in local file system when creating archive", so that if another partition is mounted somewhere under these paths, it doesn't get swallowed by accident. Before you decide anything, look at the sizes:
du -sh /etc /var/www /home /var/lib/mysql
12M /etc
3.4G /var/www
860M /home
2.1G /var/lib/mysql
df -h /var/backups
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 79G 21G 55G 28% /
Now you have numbers: compressed, each nightly archive comes to about 1.5 to 2 GB, and keeping 14 copies means close to 28 GB. This simple calculation prevents the most common backup incident of all: the backup itself filling up the disk.
A Linux server backup script with tar and cron
The simplest dependable form of backup is a nightly compressed archive of the important paths. With the inventory you just made, write /root/backup.sh:
#!/bin/bash
set -euo pipefail
DATE=$(date +%F)
DEST=/var/backups/files
mkdir -p "$DEST"
# a rebuild shortcut (Debian/Ubuntu): what was installed here
dpkg --get-selections > /root/packages.list
tar -czf "$DEST/backup-$DATE.tar.gz" \
--one-file-system \
--exclude='*/cache/*' --exclude='*/node_modules/*' \
/etc /var/www /home /root/packages.list
# retention: delete archives older than 14 days
find "$DEST" -name "backup-*.tar.gz" -mtime +14 -delete
The switches: -c creates the archive, -z compresses it with gzip and -f names the output file. The set -euo pipefail line makes the script stop at the first error instead of quietly producing an incomplete archive. For large data sets, use --zstd instead of -z.
One point many people get wrong: you don't need -p when creating an archive. tar always records permissions and ownership; -p is an extraction option which, according to the official manual, sets the "permissions of extracted files to those recorded in the archive (default for superuser)".
Scheduling: automatic server backups with cron
A backup that depends on human memory will be forgotten sooner or later. Run crontab -e and add these two lines:
MAILTO=admin@example.com
30 3 * * * /usr/bin/flock -n /var/lock/backup.lock /root/backup.sh >> /var/log/backup.log 2>&1
Three things here are deliberate. MAILTO: according to the crontab manual, if it is defined and not empty, the output of every run is emailed to that address, so you hear about it on the night the script fails. flock -n: it will "fail rather than wait if the lock cannot be immediately acquired", so two tar processes never write to the same file at once. And the full paths to binaries: cron sets SHELL to /bin/sh, and its program search path is much shorter than your terminal's; a script that works by hand but gets "command not found" under cron is a victim of exactly this. The details are in our guide to cron jobs in Linux.
Retention policy
The find line deletes old archives so the disk doesn't fill up. One subtlety: when find counts how many 24-hour periods have passed, it ignores any fraction, so -mtime +14 in practice means files at least 15 days old. A common pattern is 7 daily copies + 4 weekly + 3 monthly. Keeping only "the latest backup" is dangerous: if data corruption is discovered a few days late, the latest backup is corrupt too.
Backing up MySQL and MariaDB databases with mysqldump
Copying the /var/lib/mysql folder directly while the service is running almost always gives you an inconsistent backup that can't be restored. The right way is a logical dump. First, put the login details somewhere safe so the password doesn't show up in your shell history or in ps output: the classic method is a /root/.my.cnf file with chmod 600, and the cleaner one is the mysql_config_editor tool, which stores the same details in .mylogin.cnf. MySQL's own documentation adds a warning: this is obfuscation, not real encryption, and it won't stop an attacker who already has access to the server.
mysql_config_editor set --login-path=backup --host=localhost --user=root --password
mkdir -p /var/backups/db
mysqldump --login-path=backup --all-databases --single-transaction \
--quick --routines --events \
| gzip > /var/backups/db/all-db-$(date +%F).sql.gz
ls -lh /var/backups/db/
-rw-r--r-- 1 root root 214M Sep 8 03:31 all-db-2026-09-08.sql.gz
The --single-transaction switch issues a BEGIN before the dump starts and takes a consistent snapshot, but only for InnoDB tables; if you still have MyISAM tables, there is no guarantee for them. --quick fetches rows one at a time so a large table doesn't swallow all of your memory. And the last two switches are needed because they are not on by default: --routines brings stored procedures and functions, and --events brings scheduled events. Without them, the dump looks complete but is missing the logic that lives in the database.
If you are on MariaDB, the tool has a new name: since version 10.5 the official name is mariadb-dump, and mysqldump is only a symbolic link; since version 11.0 that link has been deprecated and it has been removed from the official Docker image. Update old scripts so that a routine upgrade doesn't break your nightly backup. And if you have several databases, dump each one to its own file: restoring the database of one broken site shouldn't mean touching the databases of nine healthy ones.
If your database is PostgreSQL
According to the official documentation, pg_dump produces consistent output even while the database is in use and doesn't block other users' reads or writes, but it dumps only one database; for global objects such as roles and tablespaces, you need pg_dumpall:
sudo -u postgres pg_dump -Fc myapp > /var/backups/db/myapp-$(date +%F).dump
sudo -u postgres pg_dumpall --globals-only > /var/backups/db/globals-$(date +%F).sql
The -Fc format is compressed by default and lets you select and reorder just some of the objects when you restore with pg_restore.
The second copy: rsync your backups to another server
So far the backups still sit on the same server. Prepare a second server and set up password-free login, as described in our guide to SSH key login:
rsync -a --stats /var/backups/ backup@203.0.113.50:/srv/backup/web1/
Number of files: 17 (reg: 16, dir: 1)
Number of regular files transferred: 2
Total file size: 12,930,469,888 bytes
Total transferred file size: 1,847,209,984 bytes
sent 1,847,432,118 bytes received 4,210 bytes 9,213,163.43 bytes/sec
total size is 12,930,469,888 speedup is 7.00
The beauty of rsync is that it is incremental: only new or changed files are transferred. According to the official manual, the -a switch equals -rlptgoD: recursive, preserving symbolic links, permissions, times, owner and group. But it does not include ACLs or extended attributes (xattrs); if you need them, add -A and -X.
A cheap, high-value upgrade is versioned snapshots with --link-dest: unchanged files are hardlinked to the previous version instead of being copied again, so every night you get a complete folder, but the space used only grows by the size of the changes:
TODAY=$(date +%F); YEST=$(date -d yesterday +%F)
rsync -a --link-dest=/srv/backup/web1/$YEST \
/var/backups/ backup@203.0.113.50:/srv/backup/web1/$TODAY/
Moving several gigabytes every night does count against your server's traffic allowance, so take a look at our guide to VPS bandwidth usage before you schedule it. And run ls -lh on the destination once a week: a backup that stopped working three weeks ago is more dangerous than having no backup at all, because it gives you a false sense of security.
Ransomware: why the server must not be able to delete anything at the destination
This is the most important difference between "making a copy" and "having a backup": if an attacker gets root on the main server, the same SSH key and the same rsync command are in their hands too, and the first thing modern ransomware does is delete or encrypt your backups.
--delete switch for a backup destination: it removes extra files at the destination to make it a mirror of the source. But leaving it out of your cron line builds no protection at all; whoever has taken over the server can run the command with --delete themselves.Real protection has to live on the destination side, and it takes three practical forms. First, on the backup server, lock the source server's SSH key to a forced command: in the destination's authorized_keys, a restrict,command="…" prefix means that key never gets a shell at all. Second, reverse the model: instead of the web server pushing the backup, have the backup server pull it; then the web server holds no key to the destination at all. Third, keep dated versions on the destination, so that today's overwrite can't destroy yesterday's copies. The remaining hardening layers are collected in our Linux server security guide.
restic: encrypted, incremental backups
The tar approach has an obvious weakness: it builds a brand-new full archive every night, even if only two files have changed. restic brings three things together: encryption on the source side (the destination only ever sees encrypted data), deduplication, and incremental snapshots that can each be restored like a complete, independent backup.
export RESTIC_REPOSITORY=sftp:backup@203.0.113.50:/srv/restic
export RESTIC_PASSWORD_FILE=/root/.restic-pass
restic init
restic backup --exclude-file=/root/backup-excludes.txt /etc /var/www
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
restic snapshots
ID Time Host Tags Paths
-----------------------------------------------------
a1b9c73f 2026-09-08 03:31:02 web1 /etc
/var/www
7d2e4406 2026-09-07 03:31:04 web1 /etc
/var/www
-----------------------------------------------------
2 snapshots
init creates the repository once, and backup sends only the changes each time. forget --prune is the same retention policy, but built into the tool: according to the documentation, --keep-daily 7 means that for each of the last 7 days that have at least one snapshot, only the most recent snapshot of that day is kept, and --prune removes unreferenced data from the repository. RESTIC_PASSWORD_FILE reads the password from a file, which is what makes running it from cron possible.
Two serious warnings. First, the repository password: the restic documentation is explicit that losing the password means your data is irrecoverably lost, so keep that password somewhere off the server. Second, the distribution's package is usually behind: at the time of writing, Ubuntu 24.04 ships version 0.16.4 while the current stable release is 0.19.1, and the documentation itself says that in this case you can download the project's official binary and run it directly. One habit that rarely gets mentioned: restic check verifies the repository's structure but, by default, doesn't read the contents of the pack files; with restic check --read-data-subset=5%, a random portion of the actual data is read and verified each time.
tar, restic or the provider's snapshot?
None of them fully replaces the others; each one covers a different failure scenario:
| Method | What is stored | Restoring one file | Encryption at the destination | Main trap |
|---|---|---|---|---|
| tar + cron | A full archive on every run | Simple, with tar -x | None, unless you add it yourself | Space grows linearly |
| Plain rsync | A mirror of the current state | The fastest option | Only in transit over SSH | No versioning; corruption gets mirrored too |
rsync with --link-dest | Daily snapshots with hardlinks | A complete folder for every day | Only in transit | Depends on the destination supporting hardlinks |
| restic | Incremental, deduplicated snapshots | With restore --include | Yes, on the source side | Losing the repository password is the end of the road |
| Provider add-on | The machine's entire disk | No; the whole disk is overwritten | On the provider's side | Inside the same infrastructure |
A practical combination: the provider add-on as a disk-level safety net, restic for encrypted daily snapshots on a second destination, and the database dumps, which restic carries along with everything else.
How much data and how much time can you afford to lose?
Before you pick a tool, pin down two numbers. RPO (recovery point objective) is the most data you are willing to lose, and it comes straight from the gap between two backups: a nightly backup means that in the worst case a day's work is gone. RTO (recovery time objective) is the longest you can tolerate before the service is back.
For a blog, a one-day RPO is reasonable; for a store with a few hundred orders a day it is a disaster, and you need to bring the database backup interval down to a few hours, while the site files can stay nightly. On the RTO side, ask yourself: where will the replacement server come from, and with what TTL will DNS point to the new destination?
Restore testing: a backup that has never been restored is a hope, not a backup
The bitterest sentence a server admin can say is: "We had backups, but the restore failed." A corrupt file, an incomplete archive, a dump cut off halfway, a lost password: all of these only reveal themselves during a restore, which is to say at the worst possible moment. Restore drills should be as much a fixed part of your routine as the Linux server backup itself. Once a month, go through these four steps:
# 1. is the archive itself intact?
gzip -t /var/backups/files/backup-2026-09-07.tar.gz && echo "archive OK"
archive OK
# 2. list the contents
tar -tzf /var/backups/files/backup-2026-09-07.tar.gz | head -3
etc/
etc/nginx/nginx.conf
etc/nginx/sites-available/default
# 3. extract into a test dir, never into /
mkdir -p /tmp/restore-test
tar -xzpf /var/backups/files/backup-2026-09-07.tar.gz -C /tmp/restore-test
# 4. the DB dump: verify it, then restore ONE database into a scratch DB
gunzip -t /var/backups/db/all-db-2026-09-07.sql.gz && echo "dump OK"
dump OK
mysqldump --login-path=backup myshop_db | gzip > /tmp/one-db.sql.gz
mysql -e "CREATE DATABASE restore_test"
gunzip -c /tmp/one-db.sql.gz | mysql restore_test
Don't underestimate step one: gzip -t takes a few seconds and tells you right there whether the archive is half-finished. Note that -p appears in the extraction line, which is where it belongs. And be careful in step four: an --all-databases dump contains its own USE statements, so pouring it into a scratch database doesn't work and in practice overwrites your real databases. Either take a single-database dump, or restore the whole dump onto a temporary server.
After extracting, don't settle for "no errors": open a few important files, and compare the row counts of a few key tables with SELECT COUNT(*). The real test goes one step further, though: create an hourly cloud server, restore the backup onto it and see whether the site comes up; then delete the server, because powering it off doesn't stop the charges. This drill also builds the skills you will need on the day of an incident: the order of the work is almost exactly what our guide to migrating a website to a new host describes, except that the source is an archive instead of a healthy server.
When backups fail silently
The worst kind of backup failure is not a loud error; it is silence. The table below connects each symptom to its likely cause and the next thing to do:
| Symptom | Likely cause | Next step |
|---|---|---|
| No new file was created and the log is empty | cron never ran it: a relative path to a binary | Check the system log for CRON entries and write the full path |
| An archive was created, but it is only a few kilobytes | tar hit an error partway through | Add set -euo pipefail and check with gzip -t |
| The disk filled up and the site went down | The retention policy didn't run | Check the space with df -h and test the find line |
| The files came back, but the site returns a 500 error | Ownership and permissions weren't restored | Repeat the restore as root with tar -xzpf |
| The dump restores, but the stored procedures are missing | Routines and events weren't dumped | Add --routines and --events |
| The backups at the destination have suddenly vanished | --delete, or an unrestricted SSH key | Lock the key down with restrict,command and switch to a pull model |
| A server upgrade broke the nightly backup | mysqldump was renamed to mariadb-dump | Update the script and run it once by hand |
The root fix for all of these is the same: monitoring. Alert on the absence of a success message, not on the presence of an error, because on the night the script doesn't run at all, it produces no error either. Practical ways to set up these alerts are in our Linux server monitoring guide.
Backups on shared hosting and cloud servers
If your site is on shared hosting, most of this article takes a simpler form, but it doesn't go away. The panel's backup section builds a full copy of your files and databases that you can download, and the same is available in the Mehran Host panel, in the hosting service's Backups tab. But a backup that stays on the same host still counts as zero offsite copies. And if you run WordPress, wp-config.php and the wp-content/uploads folder are the two assets that can't be rebuilt; the core and plugins can be reinstalled at any moment.
On a cloud server, everything we have covered is in your hands, plus the automatic backup add-on which, at locations where it is available, you turn on with a single switch in the panel. A billing detail many people notice too late: this add-on is charged for as long as it is on, even if the server is powered off, and the powered-off server itself is still billed a percentage of its hourly rate; the only thing that stops the charges completely is deleting the server. And because restoring one of these snapshots overwrites the entire disk, restores are done through a support ticket.
Frequently asked questions
How often should I back up a Linux server?
Set the gap between backups to match the most data you are willing to lose. For most sites, one nightly backup at a low-traffic hour is enough, because in the worst case you lose a single day of work. For online stores and transactional systems, the database should be backed up every few hours, while the site files can stay on a nightly schedule. Take the number from what the business needs, not from habit.
What is the difference between full, incremental and differential backups?
A full backup stores all of the data again every time: the largest size, but the simplest restore. An incremental backup keeps only the changes since the last backup, so it is small and fast, but restoring it depends on an unbroken chain of versions. A differential backup stores the changes since the last full backup and sits between the two. Tools like restic hide this complexity: storage is incremental, yet every snapshot can be restored on its own.
Why shouldn't I copy the /var/lib/mysql folder directly?
Because a running database keeps data moving between memory and disk, so a file copy captures a half-written, inconsistent state that often won't start at all. The right way is a logical dump with the database's own tool: in MySQL and MariaDB, use the --single-transaction switch, which takes a consistent snapshot of InnoDB tables without locking your site, and in PostgreSQL, use pg_dump.
Isn't my provider's backup enough?
No, because the provider's snapshot is stored in the same infrastructure as your server, so it doesn't satisfy the "one copy off the server" rule. It also has two practical limits: it usually restores the entire disk, which makes it a poor way to get back a single file, and it isn't available at every location. The best approach is to use it as your first layer and build your own copies on top of it.
How do I keep my backups out of ransomware's reach?
Put the protection on the destination side, not in the main server's script. On the destination, lock the source server's SSH key to a forced, write-only command so that it can't delete anything, or better still, reverse the model and let the backup server pull the data itself; then an infected server has no access to the destination at all. Keep dated versions on the destination as well, so one bad night can't wipe out every copy.