Backup & security

Linux server backup guide: scripts, scheduling and restore tests

Linux server backup: a terminal card tracing one backup from a nightly tar archive and database dump to an rsync copy on a second server and an encrypted restic snapshot, confirmed at the end by a restore test

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.

💡 Tip: you don't need to back up the whole operating system. The OS can be rebuilt; what can't be rebuilt is your data: configuration, site files and databases.

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/www or /home/*/public_html: site files, uploads and .env files.
  • 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.

⚠️ Don't change the order of the commands: first create the new backup, then delete the old ones. A script that cleans up first and then fails while building the archive leaves you without a single copy.

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.

⚠️ Don't use the --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:

MethodWhat is storedRestoring one fileEncryption at the destinationMain trap
tar + cronA full archive on every runSimple, with tar -xNone, unless you add it yourselfSpace grows linearly
Plain rsyncA mirror of the current stateThe fastest optionOnly in transit over SSHNo versioning; corruption gets mirrored too
rsync with --link-destDaily snapshots with hardlinksA complete folder for every dayOnly in transitDepends on the destination supporting hardlinks
resticIncremental, deduplicated snapshotsWith restore --includeYes, on the source sideLosing the repository password is the end of the road
Provider add-onThe machine's entire diskNo; the whole disk is overwrittenOn the provider's sideInside 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.

💡 Tip: write down the date of your last successful restore test. If more than a month has passed, your backup is officially in "we hope so" territory.

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:

SymptomLikely causeNext step
No new file was created and the log is emptycron never ran it: a relative path to a binaryCheck the system log for CRON entries and write the full path
An archive was created, but it is only a few kilobytestar hit an error partway throughAdd set -euo pipefail and check with gzip -t
The disk filled up and the site went downThe retention policy didn't runCheck the space with df -h and test the find line
The files came back, but the site returns a 500 errorOwnership and permissions weren't restoredRepeat the restore as root with tar -xzpf
The dump restores, but the stored procedures are missingRoutines and events weren't dumpedAdd --routines and --events
The backups at the destination have suddenly vanished--delete, or an unrestricted SSH keyLock the key down with restrict,command and switch to a pull model
A server upgrade broke the nightly backupmysqldump was renamed to mariadb-dumpUpdate 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.

Related tutorials

Ready for some hands-on practice?

A Mehran Host hourly cloud server is ready in under 60 seconds. Practice as much as you like and pay only for the hours you use. Estimate the cost with the pricing calculator before you sign up.