Help Center

Step-by-step guides for servers, networks and your account.

Linux Server

Automated, Encrypted VPS Backups with restic and rclone (Daily, Off-Site, Tested Restores)

Automated, Encrypted VPS Backups with restic and rclone (Daily, Off-Site, Tested Restores)

The safest way to back up a Linux VPS is to send encrypted snapshots to storage outside the server every day with restic, keep a clear retention policy and test restores regularly. This guide sets that up on Debian 12/13, Ubuntu 22.04/24.04 and Rocky Linux / AlmaLinux: install restic, pick an off-site repository (S3-compatible storage, SFTP or rclone), dump databases, schedule it with a systemd timer and prove you can get your data back.

Why Your Backups Must Live Off the VPS

A copy on the same server is not a backup: whatever removes the server removes the copy too. On a JUSTG cloud VPS, several normal actions delete data permanently:

  • Change location: a fresh VPS is installed with a new IP and nothing is copied. As soon as it is ready, the old VPS and all its data are deleted.
  • Overdue renewal: an unpaid service is suspended on the due date; if the invoice is still unpaid 7 days after the due date, the service is cancelled and its data is deleted.
  • OS reinstall wipes the disk, and an Immediate cancellation deletes the service and its data as soon as it is processed.

Add a mistyped rm -rf, a broken upgrade or a hacked server, and the rule is simple: the only backup you can rely on is one you make yourself and store somewhere else. Before any relocation, reinstall or cancellation, run a backup by hand and confirm the new snapshot exists.

restic encrypts data on your VPS before upload, so the storage provider only sees encrypted blocks. It also deduplicates: after the first full upload, each daily snapshot sends only what changed.

Step 1: Install restic

Debian 12/13, Ubuntu 24.04 and EPEL ship recent enough versions:

# Debian 12/13, Ubuntu 24.04
apt update && apt install -y restic
restic version

# Rocky Linux / AlmaLinux 8, 9 (from EPEL)
dnf install -y epel-release && dnf install -y restic

Ubuntu 22.04 ships restic 0.12, which has no compression and cannot use the current repository format. There, install the official binary and verify its checksum:

# Official static binary (Ubuntu 22.04 or any distro with an old restic)
VER=0.18.1   # check https://github.com/restic/restic/releases for the latest version
cd /tmp
curl -fLO https://github.com/restic/restic/releases/download/v${VER}/restic_${VER}_linux_amd64.bz2
curl -fLO https://github.com/restic/restic/releases/download/v${VER}/SHA256SUMS
sha256sum --ignore-missing -c SHA256SUMS      # must print: OK
bunzip2 restic_${VER}_linux_amd64.bz2
install -m 0755 restic_${VER}_linux_amd64 /usr/local/bin/restic
restic version

Step 2: Choose an Off-Site Repository

The repository is where encrypted snapshots are stored. Pick one option and save it as /etc/restic/env (plain NAME=value lines).

Option A: S3-compatible object storage

Any provider with an S3 API works. Create a bucket and an access key limited to that bucket, and use the provider's endpoint in the URL:

# /etc/restic/env  (S3-compatible object storage)
RESTIC_REPOSITORY=s3:https://s3.example.com/example-backups/vps1
RESTIC_PASSWORD_FILE=/etc/restic/password
RESTIC_CACHE_DIR=/var/cache/restic
AWS_ACCESS_KEY_ID=REPLACE_WITH_ACCESS_KEY
AWS_SECRET_ACCESS_KEY=REPLACE_WITH_SECRET_KEY
# Optional: monitoring ping URL (Step 11)
HC_PING_URL=

Option B: SFTP to a second server

Use another server you control, ideally in a different location or with a different provider, with a dedicated backup user that owns /srv/restic:

# /etc/restic/env  (SFTP to a second server)
RESTIC_REPOSITORY=sftp:[email protected]:/srv/restic/vps1
RESTIC_PASSWORD_FILE=/etc/restic/password
RESTIC_CACHE_DIR=/var/cache/restic
HC_PING_URL=

# One-time SSH key login for root on the VPS
ssh-keygen -t ed25519 -N "" -f /root/.ssh/id_ed25519   # skip if the key exists
ssh-copy-id [email protected]
ssh [email protected] true      # accept the host key once

Option C: an rclone backend

rclone connects restic to dozens of services (Backblaze B2, Google Drive, OneDrive, WebDAV and more):

# Install and configure rclone (interactive wizard, run as root)
apt install -y rclone        # Rocky/Alma: dnf install -y rclone (EPEL)
rclone config                # create a remote, for example named "offsite"

# /etc/restic/env  (any rclone backend)
RESTIC_REPOSITORY=rclone:offsite:example-backups/vps1
RESTIC_PASSWORD_FILE=/etc/restic/password
RESTIC_CACHE_DIR=/var/cache/restic
RCLONE_CONFIG=/root/.config/rclone/rclone.conf
HC_PING_URL=
The credentials in /etc/restic/env can usually delete backups too, so an attacker who takes over the VPS could wipe the repository. For stronger protection, enable bucket versioning or object lock, or use an append-only target, and keep the storage account password off the server.

Step 3: Create the Password File and the Repository

install -d -m 0700 /etc/restic
openssl rand -base64 32 > /etc/restic/password
chmod 600 /etc/restic/password
nano /etc/restic/env          # paste one of the examples above
chmod 600 /etc/restic/env

# Load the settings into the current shell and create the repository
set -a; . /etc/restic/env; set +a
restic init
Without the password nobody can decrypt the backups, including you. Copy /etc/restic/password and /etc/restic/env to a password manager or another safe place off the server now: they are all you need to restore if the VPS is gone.

Step 4: Decide What to Include and Exclude

Back up configuration and data, not the whole operating system: reinstalling the OS is faster than restoring system files, and snapshots stay small.

# /etc/restic/include  - one path per line; delete lines that do not exist
/etc
/root
/home
/srv
/var/www
/opt
/usr/local
/var/spool/cron
/var/backups/db

# /etc/restic/excludes
/root/.cache
/home/*/.cache
**/node_modules
**/*.tmp
/var/www/*/cache
  • Include: /etc, home directories, web roots, application data, crontabs and the database dump folder.
  • Skip: /proc, /sys, /dev, /run, /tmp, swap files and caches. They are not on the list, so they are never read.
  • Raw database files (/var/lib/mysql, /var/lib/postgresql) copied while the database runs can be inconsistent. Back up dumps instead (Step 5).
  • Docker: do not back up /var/lib/docker as a whole; back up your compose files and the volumes or folders that hold data.

Step 5: Dump Databases Before Each Backup

This script writes compressed dumps to /var/backups/db, which is on the include list. Set MYSQL and POSTGRES to match your server:

#!/bin/bash
# /usr/local/sbin/db-dump.sh - consistent database dumps before each backup
set -euo pipefail
umask 077
MYSQL=yes        # yes / no
POSTGRES=no      # yes / no
OUT=/var/backups/db
mkdir -p "$OUT"

if [ "$MYSQL" = yes ]; then
  DUMP=$(command -v mariadb-dump || command -v mysqldump)
  "$DUMP" --all-databases --single-transaction --quick \
          --routines --events --triggers | gzip > "$OUT/mysql-all.sql.gz.tmp"
  mv "$OUT/mysql-all.sql.gz.tmp" "$OUT/mysql-all.sql.gz"
fi

if [ "$POSTGRES" = yes ]; then
  cd /tmp
  runuser -u postgres -- pg_dumpall | gzip > "$OUT/postgres-all.sql.gz.tmp"
  mv "$OUT/postgres-all.sql.gz.tmp" "$OUT/postgres-all.sql.gz"
fi

--single-transaction gives a consistent InnoDB dump without locking your site. On Debian and Ubuntu, root reaches MariaDB/MySQL through the local socket without a password; if yours needs one, put it in /root/.my.cnf (mode 600), not on the command line.

Step 6: Create the Backup Script

One script runs the dumps, the backup, retention and a weekly check, and reports to an optional monitoring URL (Step 10). Any failure stops it with an error, so problems are never silent:

#!/bin/bash
# /usr/local/sbin/restic-backup.sh - dump databases, back up, apply retention
set -euo pipefail
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
set -a; . /etc/restic/env; set +a

fail() {
  [ -n "${HC_PING_URL:-}" ] && curl -fsS -m 10 --retry 3 "$HC_PING_URL/fail" >/dev/null || true
}
trap fail ERR

/usr/local/sbin/db-dump.sh

restic backup \
  --files-from /etc/restic/include \
  --exclude-file /etc/restic/excludes \
  --exclude-caches \
  --tag daily

restic forget --prune \
  --keep-daily 7 --keep-weekly 4 --keep-monthly 6

# Sundays: verify the repository and read back 5% of the data
if [ "$(date +%u)" = 7 ]; then
  restic check --read-data-subset=5%
fi

if [ -n "${HC_PING_URL:-}" ]; then
  curl -fsS -m 10 --retry 3 "$HC_PING_URL" >/dev/null
fi

Make the scripts executable and run the first backup by hand. The first run uploads everything; later runs only send changes.

chmod 700 /usr/local/sbin/db-dump.sh /usr/local/sbin/restic-backup.sh
/usr/local/sbin/restic-backup.sh      # first run by hand, watch for errors
restic snapshots

Step 7: Schedule It with a systemd Timer (or cron)

A systemd timer is the preferred option: it logs to the journal, catches up after a missed run (Persistent=true), and lowers the job's CPU and disk priority so your site stays fast.

# /etc/systemd/system/restic-backup.service
[Unit]
Description=restic backup to off-site repository
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/restic-backup.sh
Nice=10
IOSchedulingClass=idle

# /etc/systemd/system/restic-backup.timer
[Unit]
Description=Daily restic backup

[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=30min
Persistent=true

[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now restic-backup.timer
systemctl list-timers restic-backup.timer

# Test the service once and read its log
systemctl start restic-backup.service
journalctl -u restic-backup.service -n 50 --no-pager

If you prefer cron, use this instead of the timer, not both:

# /etc/cron.d/restic-backup  (only if you do not use the systemd timer)
30 3 * * * root /usr/local/sbin/restic-backup.sh >> /var/log/restic-backup.log 2>&1
Running backups at night with low priority also keeps them gentle on the disk, which helps you stay within the fair-use rules for storage I/O.

Step 8: Retention with restic forget --prune

restic forget decides which snapshots to keep, and --prune removes data no snapshot needs any more. The policy in the script keeps:

OptionWhat it keeps
--keep-daily 7the last snapshot of each of the last 7 days
--keep-weekly 4one snapshot per week for 4 weeks
--keep-monthly 6one snapshot per month for 6 months

Preview a new policy with --dry-run before relying on it:

set -a; . /etc/restic/env; set +a
restic snapshots
# Preview what a policy would remove, without deleting anything
restic forget --dry-run --keep-daily 7 --keep-weekly 4 --keep-monthly 6
restic stats --mode raw-data

The script also runs restic check every Sunday. It verifies the repository structure, and --read-data-subset=5% downloads a different 5% of the data each time, so corruption is found before you need the backup. Run restic check by hand whenever you want extra certainty.

Step 9: Test a Restore

A backup you have never restored is only a hope. Once a month, restore something real to a scratch folder and compare it:

set -a; . /etc/restic/env; set +a
restic snapshots
restic ls latest /etc/nginx

# Restore one directory to a scratch location, then compare
restic restore latest --target /tmp/restore-test --include /etc/nginx
diff -r /etc/nginx /tmp/restore-test/etc/nginx && echo "restore OK"

# Check that the database dump is complete (last line: "-- Dump completed on ...")
restic dump latest /var/backups/db/mysql-all.sql.gz | zcat | tail -n 1
rm -rf /tmp/restore-test

Practise the bad day once too, for example on a spare VPS: a new server with only the two saved files.

# On the NEW server: install restic, then restore the two saved files
install -d -m 0700 /etc/restic
nano /etc/restic/password && nano /etc/restic/env
set -a; . /etc/restic/env; set +a

restic restore latest --target /srv/restore
rsync -a /srv/restore/var/www/ /var/www/          # copy back what you need
zcat /srv/restore/var/backups/db/mysql-all.sql.gz | mariadb     # or: mysql
Avoid restic restore latest --target / on a running system: it overwrites live files, including system configuration that may not fit the new server. Restore to a separate folder and copy back what you need.

Step 10: Get Alerted When a Backup Fails

Backups usually fail quietly: an expired key, a full bucket, a changed password. A "dead man's switch" service fixes that: the script pings a URL after each successful run, and you get an alert when a ping is missing. Self-host one (for example the open-source Healthchecks) or use any similar service, and add your check URL to /etc/restic/env:

HC_PING_URL=https://hc.example.com/ping/REPLACE-WITH-YOUR-CHECK-ID

The script calls the URL on success and $HC_PING_URL/fail on error. Set the check period to one day with a few hours of grace. Without such a service, at least check restic snapshots and journalctl -u restic-backup.service every week.

FAQ

How much storage do I need?

Roughly the size of your data for the first snapshot (often less, thanks to compression), plus a little per day for changes. Run restic stats --mode raw-data after a few weeks to see the real figure.

restic says the repository is locked. What should I do?

An earlier run was interrupted and left a stale lock. Check that no restic process is running (pgrep -a restic), then run restic unlock and start the backup again.

restic uses too much memory on a 512 MB plan. Can I reduce it?

Add GOGC=20 to /etc/restic/env so restic frees memory more often, keep the include list focused, and make sure the VPS has swap. If it still runs out of memory, upgrade the plan from the service page. For help, open a ticket: JUSTG offers 24/7 AI support by ticket, and engineers follow up when a person is needed.

Was this answer helpful?
Help Center
AlipayUnionPayVISAMastercardPayPalUSDTstripe