VPS Snaps

How to back up a Linux server with restic to S3-compatible storage

restic backs up a server into an encrypted, deduplicated repository that can live directly in an S3-compatible bucket. Put the repository URL, a password file and the bucket keys in one root-only file, run restic init once, then run restic backup on a schedule followed by restic forget --prune to apply a retention policy. Everything is encrypted on the server before upload, so if you lose the password, the backups are gone.

10 min readUpdated Checked against official documentation

Install a current restic

restic is a single binary. Ubuntu 24.04's apt offers 0.16.4; the current release in October 2026 is 0.19.1. Version 0.17.0 added --stdin-from-command and separate exit codes for a missing repository, a lock and a wrong password, all used below. Install the official build:

Terminal
curl -LO https://github.com/restic/restic/releases/download/v0.19.1/restic_0.19.1_linux_amd64.bz2
Terminal
curl -LO https://github.com/restic/restic/releases/download/v0.19.1/SHA256SUMS
Terminal
sha256sum --ignore-missing -c SHA256SUMS
Terminal
bunzip2 restic_0.19.1_linux_amd64.bz2
Terminal
sudo install -m 755 restic_0.19.1_linux_amd64 /usr/local/bin/restic

On ARM servers, use restic_0.19.1_linux_arm64.bz2. restic version confirms the install. Later, sudo restic self-update fetches the newest release and checks its GPG signature first. Only official builds have it; the Debian and Ubuntu package is built without it.

Choosing between restic, Borg and Kopia? See restic vs Borg vs Kopia.

Keep the repository, password and keys in one file

Every restic command needs to know where the repository is, its password, and the bucket credentials. Create a root-only directory and a random password:

Terminal
sudo install -d -m 700 /etc/restic
Terminal
sudo sh -c 'head -c 32 /dev/urandom | base64 > /etc/restic/password'
/etc/restic/env
RESTIC_REPOSITORY=s3:https://ACCOUNT_ID.r2.cloudflarestorage.com/my-bucket/web1
RESTIC_PASSWORD_FILE=/etc/restic/password
RESTIC_CACHE_DIR=/var/cache/restic
AWS_ACCESS_KEY_ID=your-access-key-id
AWS_SECRET_ACCESS_KEY=your-secret-access-key
Terminal
sudo chmod 600 /etc/restic/password /etc/restic/env
  • RESTIC_REPOSITORY is s3:, then the endpoint, the bucket and an optional folder. On AWS write s3:s3.us-east-1.amazonaws.com/my-bucket/web1 with your bucket's region. Elsewhere write s3:https://ENDPOINT/my-bucket/web1 with the S3 endpoint from your provider's dashboard.
  • restic needs path-style URLs. my-bucket.s3.us-west-2.amazonaws.com does not work; write s3.us-west-2.amazonaws.com/my-bucket.
  • RESTIC_PASSWORD_FILE points at the password. restic ignores the trailing newline.
  • RESTIC_CACHE_DIR fixes where restic caches metadata. Without it restic uses ~/.cache/restic, but a systemd service without User= has no $HOME, so restic stops with unable to locate cache directory.
  • AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY hold the bucket key on every provider. With no region set, restic uses us-east-1; set AWS_DEFAULT_REGION if your provider needs another. Cloudflare R2 accepts us-east-1 as an alias for its auto region.

To run commands by hand, open a root shell and load the file. set -a exports every variable it defines, so restic sees them:

Terminal
sudo -i
Terminal
set -a; . /etc/restic/env; set +a

restic encrypts everything with this password and there is no recovery. Its own warning reads: "Losing your password means that your data is irrecoverably lost." Copy the password into a password manager now. A copy that lives only on the server dies with the server.

Create the repository

Terminal
restic init

On AWS, restic creates the bucket in the endpoint's region if it does not exist. Give each server its own folder, and ideally its own bucket key: anything holding the password and a key that can delete objects can wipe every snapshot in the repository.

A repository can have several passwords. restic key add adds one and restic key list shows them, so you can rotate a password without re-uploading anything. Each one unlocks the same data, so guard them all equally.

Back up

Terminal
restic backup --one-file-system --exclude-caches --exclude-file /etc/restic/excludes --tag scheduled /etc /var/www /root
  • The paths at the end are what to save. Each run creates a snapshot; files that have not changed are not uploaded again.
  • --one-file-system stays on the file systems of the paths you list, so a volume mounted below them, or /proc when backing up /, is skipped. List a mount point explicitly to include it.
  • --exclude-caches skips the contents of any directory holding a CACHEDIR.TAG file.
  • --exclude-file reads one pattern per line. --exclude takes a single pattern and can be repeated.
  • --tag labels the snapshot so snapshots, forget and restore can filter by it.
/etc/restic/excludes
# one pattern per line; lines starting with # are ignored
*.log
*.tmp
node_modules
/var/www/**/cache
/root/.cache

Patterns match whole path components against the full path. A leading / anchors a pattern at the root, * never crosses a /, and ** matches any depth, so /var/www/**/cache skips every cache folder below /var/www. Excludes never remove a path you name on the command line. Add --dry-run -vv to see what a run would add.

The exit code tells a script what happened: 0 for a complete snapshot, 3 when some files could not be read (the snapshot is saved without them), and 1 when no snapshot was made.

For a database, let restic run the dump. If the command exits non-zero, restic cancels the snapshot; piping pg_dump | restic backup --stdin would save whatever arrived, even an empty file:

Terminal
restic backup --tag db --stdin-filename mydb.sql --stdin-from-command -- sudo -u postgres pg_dump mydb

Everything after -- is the command. See backing up PostgreSQL with pg_dump for dump options.

List snapshots

Terminal
restic snapshots

It shows each snapshot's ID, time, host, tags, paths and size. --host, --tag and --path filter the list, and restic find wp-config.php searches every snapshot for a file.

Apply a retention policy with forget --prune

Terminal
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
  • --keep-daily 7 keeps the newest snapshot from each of the last 7 days that have one. --keep-weekly and --keep-monthly do the same per week (Monday to Sunday) and per calendar month. A snapshot that any rule keeps stays.
  • forget only removes snapshot records. --prune then deletes the data no remaining snapshot uses. Without it, the bucket never shrinks.
  • The policy applies separately to each group of snapshots with the same host and paths. The database snapshots above (path /mydb.sql) form their own group, so they never crowd out the file snapshots.

Preview a new policy with --dry-run. Prune locks the repository, so no backup can finish meanwhile, and it downloads and re-uploads partly used pack files, which is egress on providers that charge for it. By default it leaves up to 5% unused space to limit that (--max-unused).

On Backblaze B2 through its S3 API, restic's deletes only hide old file versions, and B2 keeps hidden versions by default. The restic docs recommend the lifecycle rule "Keep only the last version of the file" so pruned data is really removed.

Check the repository

Terminal
restic check

By default check verifies the repository's structure: the index, snapshots and directory trees. It does not download your file data. To read real data too, check a random 5% of it on each run:

Terminal
restic check --read-data-subset=5%

--read-data reads everything, which means downloading the whole repository. --read-data-subset=1/5 through 5/5 covers it in five runs. The restic docs advise a check after every prune. A check proves the repository is sound; a test restore proves you can get your files back.

Restore files

Terminal
restic restore latest --target /srv/restore --include /var/www/site/wp-config.php

Files keep their full path under the target: this one lands at /srv/restore/var/www/site/wp-config.php. latest is the newest snapshot; --host web1 or --path /var/www narrows which one, or use an ID from restic snapshots. Without --include, the whole snapshot is restored. Restore into an empty directory and copy files into place, because restoring over existing files overwrites them.

To put one folder's contents straight into the target, name the folder after the snapshot:

Terminal
restic restore latest:/var/www/site --target /srv/restore/site

A dump saved with --stdin-from-command comes back with dump, which prints one file to standard output. --path picks the newest snapshot of the dump rather than of your files:

Terminal
restic dump --path /mydb.sql latest mydb.sql > /srv/restore/mydb.sql

To browse snapshots instead, mount the repository with FUSE. It needs the fuse kernel module and fusermount on the PATH, holds the terminal until you press Ctrl-C, and is much slower than restore for more than a few files:

Terminal
mkdir -p /mnt/restic
Terminal
restic mount /mnt/restic

Clear a stale lock

Every restic command writes a lock file into the repository. A run that was killed leaves its lock behind, and the next run fails with repository is already locked by and exit code 11. restic treats a lock as stale when it is older than 30 minutes, or when it was made on the same host by a process that no longer exists. This removes stale locks only:

Terminal
restic unlock

restic unlock --remove-all removes every lock. Use it only when you are sure no restic process anywhere is using the repository, a prune above all. In scripts, --retry-lock 10m waits up to ten minutes for a lock instead of failing at once.

Schedule it with a systemd timer

/etc/systemd/system/restic-backup.service
[Unit]
Description=restic backup
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
EnvironmentFile=/etc/restic/env
ExecStart=/usr/local/bin/restic backup --one-file-system --exclude-caches --exclude-file /etc/restic/excludes --tag scheduled /etc /var/www /root
ExecStart=/usr/local/bin/restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
ExecStart=/usr/local/bin/restic check --read-data-subset=5%%
/etc/systemd/system/restic-backup.timer
[Unit]
Description=Daily restic backup

[Timer]
OnCalendar=*-*-* 02:30:00
RandomizedDelaySec=15m
Persistent=true

[Install]
WantedBy=timers.target
  • Type=oneshot allows several ExecStart lines. They run in order, and if one fails the rest are skipped, so a failed backup never goes on to prune. Exit code 3 counts as a failure too, which is a run you want to hear about.
  • %% is a literal %. systemd reads a single % as a specifier.
  • EnvironmentFile reads the same /etc/restic/env. Its lines are plain NAME=value, with no export.
  • Persistent=true starts a missed run at boot if the server was off at 02:30. RandomizedDelaySec=15m adds up to 15 minutes, so many servers do not all hit the bucket at once.
Terminal
sudo systemctl daemon-reload
Terminal
sudo systemctl enable --now restic-backup.timer
Terminal
sudo systemctl start restic-backup.service

The last command runs it once now. systemctl list-timers restic-backup.timer shows the next run, and journalctl -u restic-backup.service shows what happened. Cron works too: see how to schedule backups with cron.

Stop the server from deleting its own backups

The server holds the password and a bucket key, so an attacker with root on it can delete every snapshot. restic's documented answer is an append-only backend, its own rest-server with --append-only or rclone serve restic --append-only, which accepts new backups and refuses deletes. forget and prune then run from a separate, well-secured machine with full access.

The S3 backend has no append-only mode, so protect the bucket itself with versioning or Object Lock; see protecting backups from ransomware. With append-only, the restic docs warn that an attacker can add fake snapshots that push real ones out of a --keep-daily policy, and recommend --keep-within rules instead.

Common errors

ErrorFix
Fatal: repository does not exist: unable to open config file and Is there a repository at the following location?Exit code 10. RESTIC_REPOSITORY is wrong, uses a virtual-hosted URL, or restic init never ran there.
wrong password or no key foundExit code 12. The password file matches no key in the repository. Check RESTIC_PASSWORD_FILE.
repository is already locked byExit code 11. Another run is active, or a killed run left a lock. Wait, or run restic unlock.
at least one source file could not be readExit code 3. The snapshot was saved without some files; the lines above name them. Usually permissions, or a listed path that does not exist.
unable to locate cache directoryNo $HOME, as under systemd. Set RESTIC_CACHE_DIR.

Frequently asked questions

Does restic encrypt backups?
Always. Data is encrypted on the server before upload with a key protected by your repository password. There is no unencrypted mode and no password recovery.
What is the difference between restic forget and prune?
forget removes snapshot records according to your policy. prune deletes the stored data no remaining snapshot uses. forget --prune does both.
Can several servers share one restic repository?
Yes, and identical files are then stored once. But every server holding the password and a delete-capable key can remove all of them, so separate folders per server are safer.
How do I remove a stale restic lock?
Run restic unlock. It removes locks older than 30 minutes or left by dead processes on the same host. Use --remove-all only when nothing is running.

How this was checked

Commands, limits and prices were checked against these official pages, on October 3, 2026: