← Back to blog

How to Automate Backups for Your VPS Server

Maria Ilinca Bostan

An automated backup that has never been restored is a hypothesis, not a backup. A working routine needs four properties: it runs without you, it stores copies somewhere the server cannot reach with write access, it keeps enough history to survive a corruption you notice late, and it is tested. This guide sets that up with tools already available on any Linux VPS.

The rule worth internalising

Three copies of the data, on two kinds of media, with one off-site. On a single VPS the practical translation is: the live data, a local snapshot for fast recovery, and an encrypted off-site copy for the case where the machine is gone.

The part people skip is credential scope. If the server holds credentials that can delete its backups, then anyone who compromises the server can delete them too — and ransomware operators look for exactly this. Use append-only or write-once credentials so the backup process can add and never remove.

Databases first — a filesystem copy of a running database is not a backup

Copying /var/lib/mysql while MySQL is running produces a file set that may not be consistent. Use the database's own dump tool.

#!/usr/bin/env bash
# /usr/local/bin/db-backup.sh
set -euo pipefail
DEST=/var/backups/db
STAMP=$(date +%F-%H%M)
mkdir -p "$DEST"

# MySQL / MariaDB — single-transaction avoids locking InnoDB tables
mysqldump --single-transaction --quick --routines --triggers \
  --all-databases | gzip > "$DEST/mysql-$STAMP.sql.gz"

# PostgreSQL — custom format, restorable selectively with pg_restore
sudo -u postgres pg_dumpall | gzip > "$DEST/pg-$STAMP.sql.gz"

find "$DEST" -name '*.sql.gz' -mtime +7 -delete

Store the credentials in ~/.my.cnf with mode 600 rather than on the command line, where they appear in the process list for every user on the machine.

Files, incrementally and encrypted

restic gives deduplication, encryption at rest and snapshot history in one tool, and speaks S3, SFTP, Backblaze B2 and plain filesystems.

sudo apt install -y restic

export RESTIC_REPOSITORY="s3:s3.example.com/mybucket/server1"
export RESTIC_PASSWORD_FILE=/root/.restic-pass
restic init

restic backup /etc /var/www /home /var/backups/db \
  --exclude-caches \
  --exclude '/var/www/*/cache' \
  --exclude '*.log'

# retention: keep a useful history, not everything
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

The retention policy is the part that matters most and gets the least thought. Seven daily copies do not help if a corruption started three weeks ago and you only noticed today. Monthly copies are what save you from slow, silent damage.

Scheduling it

A systemd timer is more observable than a cron line, because failures show up in systemctl rather than in an email nobody reads.

# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/db-backup.sh
ExecStart=/usr/local/bin/files-backup.sh

# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup nightly
[Timer]
OnCalendar=*-*-* 03:15:00
RandomizedDelaySec=900
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer

Persistent=true runs a missed job after the machine comes back up, which matters for anything that is not on around the clock. RandomizedDelaySec stops fifty servers all hitting the same storage endpoint at 03:15.

Know when it fails

A backup job that silently stopped six weeks ago is the worst possible state, because you believe you are protected. Two cheap safeguards:

# fail loudly in the unit
ExecStopPost=/bin/sh -c 'test $EXIT_STATUS = 0 || curl -fsS https://your-monitor/fail'

# and check age from the outside
restic snapshots --last --json | jq -r '.[0].time'

A dead-man's-switch monitor — one that alerts when it stops hearing from you rather than when something errors — is the correct shape here, because the failure mode is absence, not error.

Testing the restore

Quarterly, on a throwaway instance:

restic restore latest --target /tmp/restore-test
gunzip -c /tmp/restore-test/var/backups/db/mysql-*.sql.gz | mysql -u root test_restore

Then check that the application actually starts against the restored data. This is the single highest-value item on the page, and the most commonly skipped. With hourly billing a restore rehearsal on a matching plan costs a few cents — there is no budget argument against doing it.

What to back up, and what not to

IncludeSkip
Database dumps/proc, /sys, /dev
/etc — your configuration is your recovery timePackage caches, node_modules, build artefacts
Application data and user uploadsAnything reproducible from source control
Crontabs, systemd units, TLS certificatesLog files older than your retention needs
A written note of the restore procedureOther backups (do not nest them)

Frequently asked questions

Are provider snapshots enough?

They are excellent for fast whole-machine rollback and useless if your account is compromised or the data corruption predates the snapshot window. Use them alongside an independent off-site copy, not instead of one.

How often should backups run?

As often as the data you are willing to lose. Nightly is the common default; a busy database usually wants continuous log archiving on top so recovery is measured in minutes of loss, not a day.

Where should backups live?

Not on the server, and ideally not with the same provider. Object storage with append-only credentials is the practical choice for most people.

Should backups be encrypted?

Yes, and client-side, before they leave the machine. restic does this by default. Store the passphrase somewhere other than the server it protects.

Next steps

Backups are one layer of a working setup. Pair them with server hardening so you need them less often, and with a realistic view of uptime so you know what recovery time you are designing for. Every command above runs on a voxa.host KVM VPS with full root access, in any of twelve locations.

Ready in under a minute

Deploy your first server now.

No contracts, no minimums. Start on an Ion KVM VPS at €0.0063 an hour and move to a monthly bare-metal server the day you outgrow it.

$ voxa deploy --plan ion --location amsterdam

KVM VPS billed hourly, capped monthly · Dedicated billed monthly · No setup fee

Included on every plan
Free IPv4 + IPv6
Every VPS
Unmetered traffic
1–10 Gbps
DDoS mitigation
2.5 Tbps
Root / IPMI access
Included
Setup fee
€0.00
Minimum term
None