MHOSTMHOST

Incremental backups with restic

restic is a command-line backup tool that encrypts your data on the client side, deduplicates it in storage, and after the first full snapshot uploads only what changed. In this article you'll install restic on your mHost VPS, create a repository (on a local disk, on another server over SFTP, or in S3), take and restore a backup, and set up a retention policy so old snapshots don't pile up forever.

What you'll need

  • An mHost VPS running Ubuntu 22.04/24.04, Debian 11/12, or AlmaLinux/Rocky Linux 8/9.
  • SSH access as root or a user with sudo.
  • Somewhere to put the backups — one of the options from step 3: a separate mounted disk on this same server, a second server (for example, another mHost VPS) reachable over SSH, or a bucket in S3 or S3-compatible storage with access keys.
  • The data you want to protect — the examples below use /etc and /var/www; replace them with your own paths.
Tip: the commands below assume you're root — backing up /etc requires root anyway (it contains files regular users can't read), and the cron job from the last step will also run as root. If you're on a sudo account, it's simplest to run sudo -i once to get a root shell and follow the article as written: environment variables you export (steps 2–3) aren't passed to a command through a separate sudo call unless you add the -E flag.

How it works: encryption, deduplication, incremental backups

  • Encryption. restic encrypts data on the client, before it ever leaves for the repository — AES-256 in counter mode (CTR) plus Poly1305-AES as the MAC for integrity checking. Even if the repository sits on someone else's server or in a public cloud, its contents can't be read without the password.
  • Deduplication. Files are split into variable-length chunks (content-defined chunking, based on Rabin fingerprints) — typically 512 KiB to 8 MiB, averaging around 1 MiB; files smaller than 512 KiB aren't split at all. Every chunk is addressed by its SHA-256 hash, and identical chunks — even from different files and different snapshots — are physically stored in the repository only once.
  • Incremental backups. On every restic backup run, restic reads all the files again (there's no other way to tell what changed), but thanks to deduplication it only uploads the chunks the repository doesn't already have — effectively just the changes. Yet every snapshot in the list still looks like a complete, self-contained backup: to restore, you don't need to hunt down a "full" backup and manually replay a chain of incrementals on top of it, the way classic full+incremental schemes work — you just pick any snapshot and restore straight from it.

Step 1. Install restic

There's no ready-made VMmanager 6 recipe for restic (unlike, say, Zabbix or LEMP — see the full list of one-click installs in "Ready-made VMmanager 6 recipes") — restic installs with a single command from the standard repositories.

Ubuntu / Debian

bash
apt update
apt install -y restic

AlmaLinux / Rocky Linux

The restic package isn't in the base repositories — enable EPEL first:

bash
dnf install -y epel-release
dnf install -y restic

Check the version:

bash
restic version
Tip: the distro's own package can lag behind the latest restic release. If you need the current version specifically, download the binary from the releases page at github.com/restic/restic/releases, then keep it updated with restic self-update (it verifies the GPG signature and drops in the new binary itself).

Step 2. Set the repository password

The repository password is what restic derives the encryption key from. restic never stores the password anywhere in the clear, and it can't recover it for you — only you can.

Generate a random password and save it to a file that only root can read:

bash
mkdir -p /etc/restic
openssl rand -base64 48 > /etc/restic/password.txt
chmod 600 /etc/restic/password.txt

restic can take the password three ways:

  • RESTIC_PASSWORD — the password itself, straight in an environment variable (visible in ps output and shell history — not the best choice for a server);
  • RESTIC_PASSWORD_FILE (or the --password-file flag) — a path to a password file, like the one you just created;
  • RESTIC_PASSWORD_COMMAND (or the --password-command flag) — a command that prints the password to stdout, for example a call to a password manager.

Export the variable pointing at the file — from here on restic will read the password from it instead of prompting interactively:

bash
export RESTIC_PASSWORD_FILE=/etc/restic/password.txt
Important: losing the repository password means losing the backups — restic has no bypass or reset for it. Keep a copy of it somewhere other than this VPS (a password manager, for example) — if the server itself goes down, the copy living on it (/etc/restic/password.txt) goes down with it.

Step 3. Create the repository (restic init)

The repository is where restic writes the encrypted, deduplicated data. restic also supports other backends (Backblaze B2, Azure Blob Storage, Google Cloud Storage, rclone, a REST server, and more) — here we'll cover the three options a VPS setup needs most often. Pick one.

Local disk

Good for a separate mounted disk on this same server — for quick local restores, for instance. It's not enough on its own: if the server itself is lost or fails, the repository goes with it — for real protection you also need a copy stored off this server (SFTP or S3, see the options below).

bash
mkdir -p /mnt/backup/restic-repo
export RESTIC_REPOSITORY=/mnt/backup/restic-repo
restic init

Over SFTP to another server

You'll need a second server (for example, another mHost VPS in a different location) with SSH and passwordless key-based login — without it, a scheduled automatic backup will just stall waiting for a password prompt. The resticbackup user below is an ordinary user on the destination server (root works too, but a dedicated user is safer) — create it in advance if it doesn't exist yet.

Generate a dedicated key and copy it to the destination server:

bash
ssh-keygen -t ed25519 -f ~/.ssh/restic_backup -N ""
ssh-copy-id -i ~/.ssh/restic_backup.pub resticbackup@backup.example.com

Add settings for this host to ~/.ssh/config — restic uses plain SSH, so it picks these up automatically:

bash
cat >> ~/.ssh/config <<'EOF'
Host backup.example.com
    IdentityFile ~/.ssh/restic_backup
    ServerAliveInterval 60
    ServerAliveCountMax 240
EOF
Tip: ServerAliveInterval/ServerAliveCountMax keep SSH from dropping the connection on a timeout while restic spends a long time processing a large amount of unchanged data without sending anything over the wire.

Create the repository:

bash
export RESTIC_REPOSITORY="sftp:resticbackup@backup.example.com:/srv/restic-repo"
restic init
Important: the destination server needs its SSH port (usually 22) open in the firewall, or restic won't be able to connect. See how to open a port in "Setting up the UFW firewall".
Tip: SFTP servers don't expand ~ as a home directory — if the path in RESTIC_REPOSITORY doesn't start with /, restic treats it as relative to the resticbackup user's home directory on the destination server.

In S3 or S3-compatible storage

Works with Amazon S3 or any S3-compatible object storage — only the URL syntax differs.

Set the access keys:

bash
export AWS_ACCESS_KEY_ID=<MY_ACCESS_KEY>
export AWS_SECRET_ACCESS_KEY=<MY_SECRET_ACCESS_KEY>

Amazon S3:

bash
export RESTIC_REPOSITORY="s3:s3.us-east-1.amazonaws.com/mhost-backups"
restic init

Another S3-compatible storage — use the provider's endpoint instead of the AWS address:

bash
export RESTIC_REPOSITORY="s3:https://s3.example-provider.com/mhost-backups"
restic init
Tip: for Amazon S3, restic will create the bucket itself if it doesn't already exist. Not every S3-compatible provider can do that (or it needs separate permissions) — it's more reliable to create the bucket yourself in the provider's dashboard beforehand. If the provider defaults to a different region, set it with AWS_DEFAULT_REGION or the -o s3.region="your-region" flag.

Once the repository exists, save both variables to a file — you'll need it in new SSH sessions and again in step 8 for cron:

bash
cat > /etc/restic/env <<'EOF'
export RESTIC_REPOSITORY="sftp:resticbackup@backup.example.com:/srv/restic-repo"
export RESTIC_PASSWORD_FILE=/etc/restic/password.txt
EOF
chmod 600 /etc/restic/env
Tip: put whichever repository address you chose above into RESTIC_REPOSITORY (a local path, sftp:..., or s3:...). In a new SSH session, source /etc/restic/env is enough — the restic commands below will work again without a -r flag or a password prompt.
Done: restic init finishes without errors and reports created restic repository ... at ... — the repository is created and ready to receive backups.

Step 4. Take your first backup

Exclusions are files and folders you don't need backed up (logs, temp files, and the like). Create /etc/restic/excludes.txt:

bash
cat > /etc/restic/excludes.txt <<'EOF'
*.log
*/node_modules
*/.git
EOF

Run your first backup — say, the server config and the site's files:

bash
restic backup --tag daily --exclude-file=/etc/restic/excludes.txt /etc /var/www

restic walks the given paths, encrypts the data, and uploads it to the repository, then prints a summary at the end: how many files were new/changed/unmodified, how much data was added to the repository (Added to the repository: ...), and the ID of the new snapshot.

Tip: you can exclude a single path or pattern right on the command line, without a file — --exclude="*.tmp" (the flag can be repeated). --tag can also be repeated if you need more than one tag.

Run the same command again without changing any files:

bash
restic backup --tag daily --exclude-file=/etc/restic/excludes.txt /etc /var/www

The second time around, restic still reads every file again (there's no other way to know what changed), but thanks to deduplication there's almost nothing to upload: the summary will show Added to the repository: 0 B (0 B stored). That's incremental backup in practice, not just in theory.

Done: the snapshot is created. Next: how to list snapshots and restore from them.

Step 5. List your snapshots

bash
restic snapshots

The command prints a table of every snapshot in the repository: a short snapshot ID, the date and time it was taken, the host, tags (if you set any with --tag), and the paths it covers. You'll need a snapshot ID (the first few characters are enough if they're unique in the repository) in the next step to restore a specific version.

Step 6. Restore your data

Restore the latest snapshot:

bash
restic restore latest --target /tmp/restore

Or a specific one, by ID from restic snapshots:

bash
restic snapshots
restic restore 79766175 --target /tmp/restore

restic doesn't overwrite files in place — it recreates the full original path inside --target. For example, if you backed up /etc, restoring with --target /tmp/restore puts the config files at /tmp/restore/etc/.... Compare the restored files against the current ones and copy over what you need by hand — that way a bad restore can't accidentally clobber a working configuration.

Restore just part of a snapshot — a single directory, say — with the --include flag:

bash
restic restore latest --target /tmp/restore --include /etc/nginx
Done: the files show up under --target — the restore worked. If you're restoring onto a different server (say, during a migration), copy them from there to their permanent location with plain cp/rsync.

Step 7. Retention policy: forget --prune

Without a retention policy, every backup adds another snapshot and the list grows forever. The forget command removes old snapshots according to rules you set, but the space taken up by data that's no longer needed is only actually freed by prune — either as a separate command, or right away if you add the --prune flag to forget.

First check that the policy will remove exactly what you expect — with --dry-run nothing is actually deleted:

bash
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --dry-run

If the result looks right, run it for real — with --prune right away, to free the space taken by the removed snapshots:

bash
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
  • --keep-daily 7 — keep one snapshot for each of the last 7 days that had a backup;
  • --keep-weekly 4 — plus one snapshot for each of the 4 preceding weeks;
  • --keep-monthly 6 — plus one snapshot for each of the 6 preceding months.

A snapshot is kept if it matches at least one of the conditions (the conditions are combined with OR, not all at once). There's also a simpler option — --keep-last N, which just keeps the N most recent snapshots regardless of the calendar. By default restic groups snapshots by host and by path set, and applies the policy to each group separately — that matters if several servers share the same repository, or if it holds backups of different sets of paths.

Important: forget without --dry-run removes snapshot records immediately, with no undo (short of restoring from a separate copy of the repository). And prune itself can tie up disk, CPU, and network for a while — it rearranges data inside the repository, which is especially noticeable over SFTP/S3 with a large amount of data. Schedule prune for a quiet time of day on the server.

Step 8. Automate it with cron

Wrap the backup and the cleanup of old snapshots into one script:

bash
cat > /usr/local/bin/restic-backup.sh <<'EOF'
#!/bin/bash
set -euo pipefail
source /etc/restic/env

restic backup --tag daily --exclude-file=/etc/restic/excludes.txt /etc /var/www
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
EOF
chmod +x /usr/local/bin/restic-backup.sh

Test it by hand before you hand it off to cron:

bash
/usr/local/bin/restic-backup.sh

Add a cron job — for example, every night at 03:00:

bash
crontab -e
cron
0 3 * * * /usr/local/bin/restic-backup.sh >> /var/log/restic-backup.log 2>&1
Tip: cron syntax, log rotation, and getting notified when a backup fails are a separate topic, covered in detail in "Scheduled backups with cron".
Done: backups now run and clean up after themselves, with no manual step needed.

What's next

For the full rundown on automating this with cron, see "Scheduled backups with cron". If a simple file mirror to another server is all you need, without encryption or deduplication, see "Backups with rsync".