VPS backups: strategies and tools
On a VPS, backups are your job, even if the plan says "backups included". We go through what exactly to back up, why a host snapshot is not enough, how to set up restic or BorgBackup in half an hour, and how to make sure you can actually restore from a backup.
People usually remember backups at the moment they need one: an update broke the database, someone deleted the site folder, the host's disk failed or the server was hacked. In each of these cases what saves you is not just any copy, but one that is stored somewhere else, was made recently and that you have restored from at least once. Below is how to set up such a backup on an ordinary Linux server.
A host snapshot is not a strategy yet
Many hosts offer disk snapshots and automatic backups in their control panel. This is useful, but such a copy has three weak points.
- It is stored at the same host. A billing error, a closed account, a data center outage or a stolen control panel password take away both the server and all its snapshots at once.
- There are few copies and they are infrequent. Often it is one or two latest copies made once a day or once a week. If a failure is noticed a few days later, there may be no good version left.
- The database in a snapshot is not always intact. A snapshot captures the disk at an arbitrary moment, as if the server had suddenly been switched off. InnoDB and PostgreSQL usually recover from this, but there is no guarantee, and MyISAM tables may end up corrupted.
A snapshot is handy as a quick restore point before an update. Your main backups are worth making with your own tools and storing separately.
The 3-2-1 rule
The classic scheme goes like this: three copies of the data, on two different media or systems, one of them in another location. For a VPS this usually looks like this:
- The working data on the server itself.
- A copy on another server or in S3 object storage at another host, or at least in another data center.
- One more copy: snapshots in the host's panel, a second backup server or a download to a storage device at home.
It is worth adding one more rule: at least one copy must be impossible to delete from the production server itself. If an attacker gets root, they also see the passwords to the backup storage. How to set this up is covered below in the section on protection from deletion.
What to back up
There is usually no need to copy the whole system: a clean server comes up from a template in minutes. What is valuable is the data and the settings.
| What | Where it usually lives | How to back it up |
|---|---|---|
| Site files and user uploads | /var/www, /home | As files |
| Databases | /var/lib/mysql, /var/lib/postgresql | Only as a dump, not as files |
| System and service settings | /etc | As files, together with the certificates from /etc/letsencrypt |
| Scheduled tasks | /etc/cron.d, users' crontab -l | As files or exported to text |
| Container data | Docker volumes, usually /var/lib/docker/volumes | As files with the container stopped, or as a dump of the database inside it |
Exclude caches, temporary files, logs and node_modules: they bloat the backup and rebuild themselves.
Databases: dumps only
You must not copy the database files of a running server: they change while being copied, and the copy ends up inconsistent. The right way is a dump. For MySQL and MariaDB:
mysqldump --single-transaction --routines --triggers shop | gzip > /var/backups/shop-$(date +%F).sql.gz
The --single-transaction flag makes a consistent dump of InnoDB tables without stopping writes. It does not protect MyISAM tables: if you have any, convert them to InnoDB or dump with --lock-all-tables. On Debian and Ubuntu root logs in to MariaDB without a password through the socket, so the command works as is when run as root; for another user, put the login and password in ~/.my.cnf with permissions 600.
For PostgreSQL:
sudo -u postgres pg_dump -Fc shop > /var/backups/shop-$(date +%F).dump
sudo -u postgres pg_dumpall --globals-only > /var/backups/pg-globals.sql
The second file saves roles and privileges that are not included in the dump of a single database. Such a dump is restored with pg_restore -d shop file.dump. If the database is tens of gigabytes and the dump takes too long, look at physical backups: mariabackup or Percona XtraBackup for MySQL, pgBackRest for PostgreSQL.
Put the dumps in one folder, for example /var/backups, and include it in the file backup, so everything goes to storage with a single command.
Tools: rsync, restic, BorgBackup
| rsync | restic | BorgBackup | |
|---|---|---|---|
| Backup versions | Only manually via --link-dest | Yes, snapshots | Yes, archives |
| Encryption | No, only in transit over SSH | Always, on the source server | Yes, optional when creating the repository |
| Deduplication | No | Yes | Yes, plus compression |
| Where it can store | Disk, SSH | Disk, SFTP, S3, its own REST server | Disk, SSH with borg on the other side |
| Append-only mode | No | Via rest-server | Via borg serve --append-only |
rsync: a mirror, not a backup
rsync syncs folders quickly, but on its own it is not a backup: a file deleted or damaged on the server will be deleted or damaged in the mirror on the next run too. You can get versions with the --link-dest flag: each run creates a new folder, and the unchanged files in it are hard links to the previous one, so space is used only for changes. It is more convenient to run this on the backup server, which pulls the data from the production server itself:
rsync -a --delete --link-dest=/backup/web/2026-10-30 root@web.example.com:/var/www/ /backup/web/2026-10-31/
This is enough for a single site, but rsync gives you neither encryption nor integrity checks.
restic
restic is a single program with no dependencies. It encrypts data before sending it, stores only new chunks of files and can write to SFTP, S3-compatible storage or its own rest-server. The repository is created once, after that a backup takes one command:
export RESTIC_REPOSITORY=sftp:backup@storage.example.com:/srv/restic/web1
export RESTIC_PASSWORD_FILE=/root/.restic-pass
restic init
restic backup /etc /var/www /var/backups --exclude-caches
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
restic check
The forget command keeps 7 daily, 4 weekly and 6 monthly snapshots, and --prune frees the space taken by the rest. Without the password from /root/.restic-pass the repository cannot be decrypted at all, so keep a copy of it off the server, for example in a password manager.
BorgBackup
Borg is similar to restic, but usually compresses better and is faster with large numbers of small files. It keeps the repository on a disk or on another server over SSH, where borg must also be installed; it does not support S3 directly. The stable branch in 2026 is 1.4; version 2.0 is still in beta and not recommended for production backups. Commands for 1.4:
export BORG_REPO=ssh://backup@storage.example.com/./web1
export BORG_PASSCOMMAND="cat /root/.borg-pass"
borg init --encryption=repokey-blake2
borg create --stats --compression zstd ::'{hostname}-{now}' /etc /var/www /var/backups
borg prune --keep-daily 7 --keep-weekly 4 --keep-monthly 6
borg compact
Since 1.2, space after prune is freed only by the compact command. In repokey mode the key is stored in the repository itself, protected by the passphrase; to be safe, export it with borg key export and keep it off the server.
If the server has a control panel, look at its built-in backups: in ISPmanager, FASTPANEL and aaPanel they are configured in the interface and can send archives to external storage. Plans with panels: ISPmanager, FASTPANEL, aaPanel.
Where to store backups
- A separate VPS at another host. Backups do not need a fast CPU, they need a large disk. Such plans are collected in VPS for data storage, VPS with HDD and VPS for backups.
- S3 object storage. You pay for the gigabytes used, and restic writes there directly. If the storage supports object versioning or a time-limited delete lock, that also protects against deletion.
- A storage device at home or in the office. Good as a third copy: the backup server pulls the data over SSH itself, and nobody can connect to it from outside.
If the database holds personal data of Russian citizens, keep Federal Law 152-FZ in mind: backups are also better kept on servers in Russia, see VPS for 152-FZ.
Protection from deletion
The worst case is when an attacker or a faulty script deletes both the data and the backups. There are three ways to prevent this.
- Pull, do not push. The backup server connects to the production server itself and pulls the data. The production server has no access to the backups at all.
- Append-only mode. For borg, the production server's key in
authorized_keyson the backup server is given a command:command="borg serve --append-only --restrict-to-path /srv/borg/web1",restrict ssh-ed25519 AAAA…. New archives can be added, old ones cannot be deleted. For restic the same is done by rest-server started with the--append-onlyflag. Old backups are then pruned from a trusted machine with a different key. - Versioning and delete lock in S3. The storage itself keeps previous versions of objects for a set period.
Schedule and how often to back up
The frequency depends on how much data you are prepared to lose. For a blog, a daily backup is enough. For a shop where orders come in all day, dump the database every hour and back up the files once a day. Run the backup from cron or a systemd timer at night, for example with a /etc/cron.d/backup file containing the line 30 3 * * * root /usr/local/bin/backup.sh.
A backup that has quietly stopped running is worse than no backup: you think you are protected. At the end of the script, send a signal to external monitoring (a plain curl to the check URL), and if the signal does not arrive on time, the monitoring will message you.
Test the restore
Once a month, restore a backup to a clean server and check that the site opens and the data is in place. Hourly billing is convenient for this: you pay for the test server only for the hours it runs.
restic restore latest --target /tmp/restore --include /var/www/site
restic check --read-data-subset=10%
borg extract ::web1-2026-10-31T03:30:00 var/www/site # archive name comes from borg list
borg check --verify-data
The second and fourth commands read the actual data in the repository, not just its index, and find damaged chunks. Write down the restore procedure in a separate file off the server: in the middle of an outage there is no time to recall commands. How to prepare the new server you will restore to is described in First 10 steps after buying a VPS: security, and moving a whole site is covered in How to move a website from shared hosting to a VPS.