Docker & DevOps

Encrypted off-site backups with restic: init, snapshot, restore

Set up restic encrypted backups on a Linux VPS — initialise a repository on a second host, take snapshots, list them, and restore data to a fresh server.

IntermediateLinux server6 min read

This guide shows you how to run restic encrypted backups on a Linux VPS: initialise a repository on a second machine, take snapshots, list them, and restore data to a fresh server. Restic encrypts everything on the client before it leaves the box, so the storage host only ever sees ciphertext.

You will use two servers: the source VPS that holds your data, and a second VPS that stores the repository. If you only have one server, you can still follow along with a local repository path, but off-site storage is the point of the exercise.

Before you start

  • Two Linux VPS running Ubuntu 22.04/24.04, Debian 12, or AlmaLinux/Rocky 8–9.
  • Root or sudo on both. The examples use 203.0.113.10 as the storage host.
  • SSH access from the source server to the storage server.
  • A password manager. The repository password is the only key to your data.

Warning: Restic has no password recovery. If you lose the repository password, every snapshot is permanently unreadable. Store it somewhere other than the server you are backing up.

Install restic on both servers

On Debian and Ubuntu:

Bash
sudo apt update
sudo apt install -y restic

On AlmaLinux and Rocky:

Bash
sudo dnf install -y restic

Check the version with restic version. Package versions differ between distributions, and restic changes repository behaviour occasionally, so check the project's own documentation if you need a specific release.

Prepare the storage host

On the storage server, create a dedicated user and directory. Keeping the repository under a separate account means a compromised source server cannot wander through the rest of the filesystem.

Bash
sudo adduser --disabled-password --gecos "" restic
sudo mkdir -p /srv/restic-repo
sudo chown restic:restic /srv/restic-repo
sudo chmod 700 /srv/restic-repo

Back on the source server, generate a key pair for the backup job and copy the public half across.

Bash
ssh-keygen -t ed25519 -f /root/.ssh/restic_backup -N ""
ssh-copy-id -i /root/.ssh/restic_backup.pub restic@203.0.113.10
ssh -i /root/.ssh/restic_backup restic@203.0.113.10 "echo connected"

The last command should print connected. If it asks for a password, the key was not installed correctly.

Initialise the restic encrypted backup repository

Create the password file first, then point restic at the SFTP backend.

Bash
openssl rand -base64 32 > /root/.restic-pass
chmod 600 /root/.restic-pass

export RESTIC_REPOSITORY="sftp:restic@203.0.113.10:/srv/restic-repo"
export RESTIC_PASSWORD_FILE=/root/.restic-pass

restic -o sftp.command="ssh -i /root/.ssh/restic_backup restic@203.0.113.10 -s sftp" init

Expect output like created restic repository 4f2a... at sftp:restic@203.0.113.10:/srv/restic-repo. The -o sftp.command flag tells restic which SSH key to use; without it, restic falls back to your default key.

Tip: Exporting RESTIC_REPOSITORY and RESTIC_PASSWORD_FILE in your shell saves typing, but scripts should set them explicitly so cron jobs work without a login shell.

Take your first snapshot

Run a backup of the directories that matter. Exclude caches and anything you can rebuild.

Bash
restic -o sftp.command="ssh -i /root/.ssh/restic_backup restic@203.0.113.10 -s sftp" backup \
  /etc /home /var/www \
  --exclude-caches \
  --exclude /var/www/*/var/cache \
  --tag daily

Restic prints a progress line and finishes with a summary: files new, files changed, data added, and total duration. The first run uploads everything; later runs only send changed blocks, so they are much faster.

List what you have:

Bash
restic snapshots
restic ls latest
restic stats

restic snapshots shows the snapshot ID, timestamp, host and tags. restic ls latest lists the contents of the most recent snapshot. restic stats reports total size and file count.

Automate snapshots and pruning

Create a small script so cron does not depend on your shell environment.

Bash
sudo tee /usr/local/bin/restic-backup.sh > /dev/null <<'EOF'
#!/bin/bash
set -euo pipefail
export RESTIC_REPOSITORY="sftp:restic@203.0.113.10:/srv/restic-repo"
export RESTIC_PASSWORD_FILE=/root/.restic-pass
export RESTIC_OPTIONS="-o sftp.command=ssh -i /root/.ssh/restic_backup restic@203.0.113.10 -s sftp"

restic backup /etc /home /var/www --exclude-caches --tag daily
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
EOF

sudo chmod 700 /usr/local/bin/restic-backup.sh

Then schedule it:

Bash
echo "0 3 * * * /usr/local/bin/restic-backup.sh >> /var/log/restic-backup.log 2>&1" | sudo tee /etc/cron.d/restic-backup

forget with --prune removes old snapshots and reclaims space. Without pruning, the repository grows forever.

Restore data to a new server

On the replacement server, install restic, copy the password file and the SSH key, then read the repository.

Bash
sudo apt install -y restic
scp root@203.0.113.10:/root/.restic-pass /root/.restic-pass
scp root@203.0.113.10:/root/.ssh/restic_backup /root/.ssh/restic_backup
chmod 600 /root/.restic-pass /root/.ssh/restic_backup

export RESTIC_REPOSITORY="sftp:restic@203.0.113.10:/srv/restic-repo"
export RESTIC_PASSWORD_FILE=/root/.restic-pass
restic snapshots

Restore into a staging directory first so you can inspect the result:

Bash
restic restore latest --target /mnt/restore

To pull back a single path, use --include:

Bash
restic restore latest --target /mnt/restore --include /var/www/example.com

Warning: Restoring with --target / writes straight over live files and can break a running service. Restore to a staging directory, compare, then move files into place with the service stopped.

If you only need a few files, mount the repository instead of restoring everything:

Bash
mkdir -p /mnt/restic
restic mount /mnt/restic
ls /mnt/restic/snapshots/latest/var/www

Press Ctrl+C to unmount when you are done.

Check it works

Confirm the repository is intact and readable:

Bash
restic check
restic check --read-data

restic check verifies structure and metadata. --read-data downloads and hashes every pack file, which proves the data itself is recoverable but takes a long time on a large repository. Run it occasionally, not every night.

Then compare snapshots to see what changed:

Bash
restic diff latest

A successful restore test is the only real proof. Restore one directory into /tmp, open a file, and confirm the contents match the source.

Troubleshooting

  • Fatal: unable to open config file or wrong password — RESTIC_PASSWORD_FILE is unset or points at the wrong file. Check with echo $RESTIC_PASSWORD_FILE and confirm the file is readable by the user running restic.
  • sftp: command not found or SSH key rejected — the storage host has the SFTP subsystem disabled, or the key permissions are wrong. Run chmod 600 /root/.ssh/restic_backup and test the SSH command on its own.
  • repository is already locked — a previous run was killed mid-backup. Confirm no restic process is running, then clear the lock with restic unlock.
  • Backups saturate the uplink — throttle with --limit-upload 2048 (KiB/s) and wrap the job in nice -n 19 ionice -c3.
  • Restored files are missing — the path was excluded at backup time. Run restic ls <snapshot-id> to see exactly what the snapshot contains before assuming data loss.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.