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
sudo apt update
sudo apt install -y restic
restic versionUbuntu 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:
- Create a private bucket,
vps-backups. - 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.
- 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:
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/passwordNow /etc/restic/env:
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_KEYsudo chmod 600 /etc/restic/envIf 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:
sudo -i
set -a; . /etc/restic/env; set +a
restic initcreated 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:
/etc
/home
/root
/var/www
/var/backups/dbAdd 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:
/root/.cache
/home/*/.cache
**/node_modulesDon'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:
#!/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"chmod 700 /usr/local/sbin/restic-backupWhat it does:
- Database dumps.
pg_dumpandmariadb-dumpwrite consistent copies to/var/backups/db, which is in the include list. Changeappdbto your database (or add a line per database). As root on Ubuntu and Debian,mariadb-dumplogs in through the Unix socket without a password. Setting up the databases themselves is in install PostgreSQL and install MariaDB. restic backup.--exclude-cachesalso skips any directory marked with a standardCACHEDIR.TAGfile. Thenightlytag 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 --helplists 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):
/usr/local/sbin/restic-backupThe output ends with the snapshot it saved, the policy applied and no errors were found. List the snapshots:
restic snapshotsID Time Host Tags Paths
------------------------------------------------------------------------
9bd2dd52 2026-10-21 23:47:17 web1 nightly /etc
/home
/root
/var/backups/db
/var/www
------------------------------------------------------------------------
1 snapshotsThe 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:
[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=idleAnd /etc/systemd/system/restic-backup.timer:
[Unit]
Description=Run the restic backup every night
[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=30m
Persistent=true
[Install]
WantedBy=timers.targetOnCalendar 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:
systemctl daemon-reload
systemctl enable --now restic-backup.timer
systemctl start restic-backup.serviceCheck it worked
systemctl list-timers restic-backup.timer
journalctl -u restic-backup.service -n 30 --no-pagerThe 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:
restic ls latest /var/wwwRestore one directory into a scratch location (not over the live copy):
restic restore latest --target /tmp/restore --include /var/www/site
ls -la /tmp/restore/var/www/sitePrint one file straight to the terminal or a pipe, for example a database dump:
restic dump latest /var/backups/db/mariadb.sql.gz | gunzip | headFor 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 --prunefrom 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
- Prefer backing up to a second server over SSH? See borgmatic with BorgBackup.
- The basics of what to back up and why are in backups you should run.
- The restic documentation covers
restic mount(browse snapshots as a folder), other backends and tuning.