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

Encrypted nightly backups to any S3-compatible bucket with restic: database dumps, a systemd timer, a retention policy and a tested restore.

Tested on Ubuntu 24.04 LTS

Before you start

  • A server running Ubuntu 24.04 or Debian
  • Root or a user with sudo
  • A bucket at any S3-compatible storage provider with an access key for it

To back up a Linux server with restic to S3, install restic, put the bucket's endpoint, access key and a repository password in a root-only environment file, run restic init once, and then have a systemd timer run restic backup every night followed by restic forget --prune to keep a sensible number of snapshots. restic encrypts everything on the server before it uploads and only sends data it hasn't stored before, so nightly runs are small and the storage provider never sees your files.

Any provider that speaks the S3 API works the same way; only the endpoint URL changes. Pick one in a different place from your server, so one incident can't take both.

The examples use these names. Replace them with yours:

What In this tutorial
The S3 endpoint https://s3.example.com (your provider's S3 endpoint)
The bucket vps-backups
The folder for this server in the bucket web1

Install restic

bash
sudo apt update
sudo apt install -y restic
restic version

Ubuntu 24.04 installs restic 0.16.4, Debian 12 has 0.14.0 and Debian 13 0.18.0. All of them do everything in this tutorial. The distribution builds leave out restic self-update (unknown command "self-update"); if you want the newest release, download the binary from restic's GitHub releases instead and verify its checksum.

Make the bucket and a key

At your storage provider:

  1. Create a private bucket, vps-backups.
  2. Create an access key that can only use that bucket (read, write, list and delete objects). Don't use an account-wide key: if the server is broken into, the key on it is the attacker's too.
  3. Note the S3 endpoint URL and, if the provider asks for one, the region.

Store the credentials on the server

Everything restic needs goes in two files only root can read. Create the directory and a strong repository password:

bash
sudo mkdir -p /etc/restic
sudo chmod 700 /etc/restic
openssl rand -base64 32 | sudo tee /etc/restic/password > /dev/null
sudo chmod 600 /etc/restic/password

Now /etc/restic/env:

ini
RESTIC_REPOSITORY=s3:https://s3.example.com/vps-backups/web1
RESTIC_PASSWORD_FILE=/etc/restic/password
AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY=YOUR_SECRET_ACCESS_KEY
bash
sudo chmod 600 /etc/restic/env

If your provider needs a region, add AWS_DEFAULT_REGION=.... The AWS_ names are just what the S3 protocol's clients read; they work with every provider.

Copy the repository password somewhere off the server now, into your password manager. The backups are encrypted with it. If the server is lost, so is the copy in /etc/restic, and without the password nobody, including your provider, can read the backups.

Create the repository

Work as root for the rest of the tutorial and load the environment file into your shell:

bash
sudo -i
set -a; . /etc/restic/env; set +a
restic init
text
created restic repository e6ad518975 at s3:https://s3.example.com/vps-backups/web1

Please note that knowledge of your password is required to access
the repository. Losing your password means that your data is
irrecoverably lost.

Run set -a; . /etc/restic/env; set +a again in any new root shell before using restic by hand.

Choose what to back up

You don't need the whole disk: the operating system comes back from a reinstall. Back up what you made. List the paths in /etc/restic/include.txt:

text
/etc
/home
/root
/var/www
/var/backups/db

Add anything else your apps keep, such as /srv, /opt/yourapp or Docker volumes under /var/lib/docker/volumes. Then the things not worth storing, in /etc/restic/exclude.txt:

text
/root/.cache
/home/*/.cache
**/node_modules

Don't list a live database's data directory (/var/lib/postgresql, /var/lib/mysql). Files copied while the database writes to them don't make a consistent copy. Dump the databases first; the next script does.

Write the backup script

Create /usr/local/sbin/restic-backup:

bash
#!/bin/bash
# /usr/local/sbin/restic-backup: dump the databases, back up, apply the retention policy.
set -euo pipefail

# 1. Database dumps (keep the lines for the databases you run).
mkdir -p /var/backups/db
chmod 700 /var/backups/db
if command -v pg_dump >/dev/null; then
  sudo -u postgres pg_dump -Fc appdb > /var/backups/db/appdb.pgdump
fi
if command -v mariadb-dump >/dev/null; then
  mariadb-dump --single-transaction --routines --events --all-databases | gzip > /var/backups/db/mariadb.sql.gz
fi

# 2. The backup itself.
restic backup \
  --files-from /etc/restic/include.txt \
  --exclude-file /etc/restic/exclude.txt \
  --exclude-caches \
  --tag nightly

# 3. Keep 7 daily, 4 weekly and 6 monthly snapshots; delete the data nothing else uses.
restic forget --tag nightly --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

# 4. Check the repository's structure, and read back a different tenth of the data each day.
restic check --read-data-subset="$(( $(date +%-d) % 10 + 1 ))/10"
bash
chmod 700 /usr/local/sbin/restic-backup

What it does:

  • Database dumps. pg_dump and mariadb-dump write consistent copies to /var/backups/db, which is in the include list. Change appdb to your database (or add a line per database). As root on Ubuntu and Debian, mariadb-dump logs in through the Unix socket without a password. Setting up the databases themselves is in install PostgreSQL and install MariaDB.
  • restic backup. --exclude-caches also skips any directory marked with a standard CACHEDIR.TAG file. The nightly tag keeps these snapshots apart from any you take by hand.
  • restic forget --prune. Applies the retention policy and deletes data no remaining snapshot uses. Change the numbers to taste; restic forget --help lists the other --keep-* options.
  • restic check. Verifies the repository's structure every night and downloads a tenth of the stored data to check it, a different tenth each day, so over a month everything is read back at least once.

Check it worked

Run it once by hand (the environment is still loaded in your root shell):

bash
/usr/local/sbin/restic-backup

The output ends with the snapshot it saved, the policy applied and no errors were found. List the snapshots:

bash
restic snapshots
text
ID        Time                 Host        Tags        Paths
------------------------------------------------------------------------
9bd2dd52  2026-10-21 23:47:17  web1        nightly     /etc
                                                       /home
                                                       /root
                                                       /var/backups/db
                                                       /var/www
------------------------------------------------------------------------
1 snapshots

The first run uploads everything; later runs only send what changed.

Run it every night with a systemd timer

A systemd timer beats cron here: it logs to the journal, catches up on a run missed while the server was off, and won't start a second copy while one is still running.

Create /etc/systemd/system/restic-backup.service:

ini
[Unit]
Description=Back up this server with restic
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
EnvironmentFile=/etc/restic/env
ExecStart=/usr/local/sbin/restic-backup
Nice=10
IOSchedulingClass=idle

And /etc/systemd/system/restic-backup.timer:

ini
[Unit]
Description=Run the restic backup every night

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

[Install]
WantedBy=timers.target

OnCalendar is in the server's time zone (UTC on most servers). RandomizedDelaySec spreads the start over half an hour, and Persistent=true runs a missed backup at the next boot. Nice and IOSchedulingClass=idle keep the backup from slowing your apps down.

Turn the timer on and start one run now, to see it work under systemd:

bash
systemctl daemon-reload
systemctl enable --now restic-backup.timer
systemctl start restic-backup.service

Check it worked

bash
systemctl list-timers restic-backup.timer
journalctl -u restic-backup.service -n 30 --no-pager

The first shows when the next run is due. The second shows the last run's output, ending with no errors were found. systemctl status restic-backup.service shows status=0/SUCCESS after a good run; a failed run leaves the unit in the failed state, which monitoring can alert on.

Restore a file, and prove you can

A backup you haven't restored from is a guess. Try it now, and again every few months.

See what is in the newest snapshot:

bash
restic ls latest /var/www

Restore one directory into a scratch location (not over the live copy):

bash
restic restore latest --target /tmp/restore --include /var/www/site
ls -la /tmp/restore/var/www/site

Print one file straight to the terminal or a pipe, for example a database dump:

bash
restic dump latest /var/backups/db/mariadb.sql.gz | gunzip | head

For a whole-server disaster, the steps are: build a new server, install restic, recreate /etc/restic/env and the password from your password manager, run restic restore latest --target /, then restore the databases from their dumps (pg_restore or mariadb). Practise it once on a spare server; it is the only way to know nothing is missing.

Protect the backups from the server

The access key on the server can delete objects, and restic forget --prune needs that. Someone who breaks into the server can therefore delete your backups too. Two ways to close that gap:

  • Turn on object versioning or object lock on the bucket, if your provider offers it, with a retention period longer than the time you'd need to notice a break-in. Deleted or overwritten objects stay recoverable.
  • Or prune from somewhere else: give the server a key that can't delete, and run restic forget --prune from another machine with a full-rights key.

Server snapshots are a useful extra, but they live with the server's provider; restic's copy is the one that survives losing the account or the server.

Troubleshooting

Fatal: wrong password or no key found. The password in /etc/restic/password isn't the one the repository was created with, or RESTIC_PASSWORD_FILE isn't set because the environment file wasn't loaded. Run set -a; . /etc/restic/env; set +a and try again.

Fatal: unable to open config file: Stat: The request signature we calculated does not match the signature you provided. Check your key and signing method. The secret key is wrong. Copy it again from your provider, without spaces or quotes.

Fatal: unable to open config file: Stat: The specified key does not exist. followed by Is there a repository at the following location?. The repository hasn't been created at that address: check the bucket and folder in RESTIC_REPOSITORY, or run restic init.

Fatal: create key in repository at ... failed: repository master key and config already initialized. You ran restic init against a repository that already exists. Nothing is wrong; leave it.

An error saying the repository is already locked. Another restic run holds the lock, or a previous run was interrupted and left it behind. Make sure no restic is running (pgrep restic), then restic unlock.

Next steps

Be first when orders open

Join the waitlist and we'll send you one email when VPSNine launches. Pick a plan if you already know which one you want, and we'll size the first servers around it.

  • One email, on launch day
  • Monthly or yearly, no lock-in
  • 14-day money-back guarantee