restic is a single-binary backup program that encrypts and deduplicates data on the client and writes it directly to local disks, SFTP servers or object storage. Because it speaks the S3 API, it is a simple way to keep an offsite copy of a server without running a second machine. In this tutorial you will back up an Ubuntu 24.04 server to an S3-compatible bucket with restic, include a database dump, schedule it with a systemd timer, apply a retention policy and restore files.

Prerequisites

To follow this guide you need:

  • A server running Ubuntu 24.04 LTS (for example a CubePath VPS) with a non-root user with sudo privileges.
  • An S3-compatible object storage bucket in a different location from the server, with:
    • the S3 endpoint URL, referred to as your_s3_endpoint (for example s3.example.com),
    • an empty bucket, referred to as your_bucket,
    • an access key and secret key with read and write access to that bucket.
  • A password manager or other safe place outside the server to store the repository password.

Step 1 - Installing restic

restic is available in the Ubuntu repositories:

sudo apt update
sudo apt install restic

Check the installed version:

restic version
restic 0.16.4 compiled with go1.22.2 on linux/amd64

The Ubuntu package is fully usable for this guide. Newer releases are published as prebuilt binaries on the restic GitHub releases page if you need a feature added later; the repository format is compatible across these versions.

Step 2 - Storing the repository settings

restic reads its repository location, password and cloud credentials from environment variables. Keeping them in one file lets you use the same settings in your shell and in the systemd service later.

Create a configuration directory that only root can read:

sudo install -d -m 700 /etc/restic

Generate a random repository password:

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

Print it once and save it in your password manager. Without this password, the backups cannot be decrypted by anyone, including you:

sudo cat /etc/restic/password

Now create the environment file:

sudo nano /etc/restic/env
RESTIC_REPOSITORY=s3:https://your_s3_endpoint/your_bucket/web01
RESTIC_PASSWORD_FILE=/etc/restic/password
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key

The path after the bucket name (web01) is a prefix inside the bucket, so several servers can share one bucket with separate repositories. The AWS_* variable names are used for every S3-compatible provider, not only AWS. Some providers also require a region; if yours does, add AWS_DEFAULT_REGION=your_region.

Protect the file, since it contains credentials:

sudo chmod 600 /etc/restic/env

Step 3 - Initializing the repository

The remaining commands run as root so restic can read every file. Open a root shell and load the settings into it:

sudo -i
set -a; source /etc/restic/env; set +a

set -a exports every variable defined while it is active, which is what restic needs. Now create the repository:

restic init
created restic repository 5a2c8e91f0 at s3:https://your_s3_endpoint/your_bucket/web01

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

If you see an access denied error instead, check the keys, the bucket name and the endpoint URL.

Step 4 - Creating your first backup

Create a list of paths to leave out:

nano /etc/restic/excludes
/home/*/.cache
/root/.cache
/var/www/*/cache
*.swp

Run the first backup of the directories that matter on a typical server:

restic backup --exclude-caches --exclude-file /etc/restic/excludes \
  /etc /home /root /var/www

--exclude-caches skips directories that contain a standard CACHEDIR.TAG file. At the end, restic prints a summary:

Files:        48213 new,     0 changed,     0 unmodified
Dirs:          6120 new,     0 changed,     0 unmodified
Added to the repository: 912.482 MiB (905.117 MiB stored)

processed 48213 files, 2.140 GiB in 1:52
snapshot 3f1a2b4c saved

Run the same command again. This time nearly every file is listed as unmodified and only a few kilobytes are added, because restic only uploads data it has not seen before.

Backing up a database

Never back up the raw files of a running database. restic can read a dump from standard input and store it as a file in the snapshot, so nothing is written to the local disk. For MySQL or MariaDB:

mysqldump --single-transaction --routines --events --all-databases \
  | restic backup --stdin --stdin-filename mysql-all.sql --tag database

For PostgreSQL, use sudo -u postgres pg_dumpall in place of mysqldump. On Ubuntu 24.04, root connects to MySQL and MariaDB through the Unix socket, so no password is needed.

Step 5 - Browsing and restoring snapshots

List the snapshots in the repository:

restic snapshots
ID        Time                 Host    Tags        Paths
------------------------------------------------------------------------------
3f1a2b4c  2026-09-25 10:30:11  web01               /etc /home /root /var/www
9c0d4e1b  2026-09-25 10:34:02  web01               /etc /home /root /var/www
e7f1a0c3  2026-09-25 10:36:45  web01   database    /mysql-all.sql
------------------------------------------------------------------------------
3 snapshots

List the files of one directory in the most recent file snapshot:

restic ls latest --path /etc /etc/nginx

--path /etc makes latest pick the newest snapshot that includes /etc, so the database snapshot is ignored.

Restore /etc/nginx into a temporary directory and compare it with the live copy:

restic restore latest --path /etc --target /tmp/restic-restore --include /etc/nginx
diff -r /etc/nginx /tmp/restic-restore/etc/nginx && echo "identical"
identical

restic recreates the full path under the target directory and restores ownership and permissions. To go back further in time, use a snapshot ID such as 9c0d4e1b instead of latest.

To restore the database dump, write it to standard output and pipe it where you need it:

restic dump latest --tag database /mysql-all.sql > /tmp/mysql-all.sql

Step 6 - Applying a retention policy

restic forget decides which snapshots to keep and --prune deletes the data only they used. Preview it first:

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

This keeps the latest 7 daily, 4 weekly and 6 monthly snapshots. restic applies the policy separately to each group of host and paths, so the database snapshots and the file snapshots each keep their own history. When the output looks right, run it for real:

restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

Step 7 - Scheduling backups with a systemd timer

A systemd service can load the same environment file you created in Step 2, so no wrapper script is needed. Each ExecStart line runs in order and the service stops at the first failure.

Create the service unit:

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

[Service]
Type=oneshot
EnvironmentFile=/etc/restic/env
Environment=RESTIC_CACHE_DIR=/var/cache/restic
CacheDirectory=restic
ExecStart=/usr/bin/restic backup --exclude-caches --exclude-file /etc/restic/excludes /etc /home /root /var/www
ExecStart=/usr/bin/restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
Nice=10
IOSchedulingClass=idle

CacheDirectory=restic makes systemd create /var/cache/restic, where restic keeps a local metadata cache that avoids downloading index data on every run. If you back up a database, add a line before the forget line that calls a small script running the mysqldump | restic backup --stdin pipeline from Step 4, because systemd does not interpret pipes in ExecStart.

Create the timer unit:

nano /etc/systemd/system/restic-backup.timer
[Unit]
Description=Nightly restic backup

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

[Install]
WantedBy=timers.target

Enable the timer and trigger one run to verify it works under systemd:

systemctl daemon-reload
systemctl enable --now restic-backup.timer
systemctl start restic-backup.service
journalctl -u restic-backup.service -n 30 --no-pager

The journal shows the backup summary, the forget report and Finished restic-backup.service. Check the next scheduled run with:

systemctl list-timers restic-backup.timer

Step 8 - Checking repository integrity

restic check verifies the repository structure. Adding --read-data-subset also downloads and verifies a portion of the actual data, which catches corruption in the storage without downloading everything each time:

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

The command lists each phase as it runs and ends with:

no errors were found

Run this monthly. Object storage providers may charge for the data downloaded by the check, so keep the percentage modest.

Leave the root shell when you are done:

exit

Troubleshooting

  • Fatal: unable to open config file: Stat: ... Access Denied: the keys do not have access to the bucket, or the endpoint or bucket name is wrong. Test the values with your provider's console.
  • Fatal: wrong password or no key found: /etc/restic/password does not match the repository. Restore the file from your password manager.
  • unable to create lock in backend: repository is already locked: another restic process is running, or one was killed. When you are sure nothing is running, remove stale locks with restic unlock.
  • The service fails with exit code 3: some files could not be read during the backup, for example because they were deleted mid-run. The snapshot was still saved; check the journal for the file names and exclude them if they are temporary.

Conclusion

Your server now uploads encrypted, deduplicated snapshots to object storage every night, keeps a defined history and verifies it can read the data back. That gives you the offsite copy of the 3-2-1 backup rule. Next, consider enabling object lock or versioning on the bucket if your provider supports it, so a compromised server cannot delete old backups, and pair restic with a fast local copy using BorgBackup for quicker restores.