Restic is a backup program that encrypts, deduplicates and compresses data before sending it to a repository, which can be a local directory, an SFTP server, any S3-compatible bucket or restic's own REST server. In this tutorial you will set up production backups on Ubuntu 24.04: a primary repository on S3-compatible storage, a systemd service and timer that back up, apply a retention policy and verify data, and a second offsite copy on an append-only REST server that a compromised client cannot delete.

Prerequisites

To follow this tutorial you need:

  • The server to back up, running Ubuntu 24.04 LTS, with a non-root user with sudo privileges. This guide calls it the client.
  • An S3-compatible bucket and an access key with read and write permissions on it (Amazon S3, MinIO or any other S3-compatible provider).
  • For Step 7, a second server running Ubuntu 24.04 to act as the backup server, for example a CubePath VPS in a different location, with Nginx installed and a subdomain such as backup.your_domain pointing to it.

All commands on the client run as root through sudo, because backing up system files requires reading everything.

Step 1 - Installing restic

Ubuntu 24.04 packages restic 0.16, which supports everything in this guide, including compression and copying between repositories:

sudo apt update
sudo apt install restic

Check the version:

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

Step 2 - Storing the repository settings

Restic reads its repository address, password and backend credentials from environment variables. Keeping them in one root-only file lets both your shell and systemd use exactly the same settings.

Create the configuration directory and a random repository password:

sudo install -d -m 0700 /etc/restic
sudo sh -c 'openssl rand -base64 32 > /etc/restic/password'
sudo chmod 600 /etc/restic/password

Create the environment file:

sudo nano /etc/restic/restic.env
RESTIC_REPOSITORY=s3:https://s3.your_provider.com/your_bucket/your_hostname
RESTIC_PASSWORD_FILE=/etc/restic/password
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key

The repository URL is s3: followed by the provider's endpoint, the bucket name and an optional path. Using the host name as the path lets several servers share one bucket. For Amazon S3, use s3:https://s3.amazonaws.com/your_bucket/your_hostname and, if the bucket is not in us-east-1, add AWS_DEFAULT_REGION=your_region.

Restrict the file:

sudo chmod 600 /etc/restic/restic.env

To run restic commands interactively with these settings, start a root shell and load the file:

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

The remaining client commands in this tutorial assume you are in this root shell.

Step 3 - Initializing the repository

Create the repository structure and encryption keys in the bucket:

restic init
created restic repository 3f8a1c2b9d at s3:https://s3.your_provider.com/your_bucket/your_hostname

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

Step 4 - Running the first backup with exclusions

Decide what to back up. For a typical server that is /etc, /home, /root, /opt, /var/www and your application data. Excluding caches and temporary files saves space and time.

Create an exclude file with one pattern per line:

nano /etc/restic/excludes
/home/*/.cache
/root/.cache
/var/cache
/var/tmp
**/node_modules
**/__pycache__

Run the first backup:

restic backup --exclude-file /etc/restic/excludes --exclude-caches --one-file-system \
  --tag scheduled /etc /home /root /opt /var/www

--exclude-caches skips directories marked with a CACHEDIR.TAG file, and --one-file-system stops restic from descending into other mounts such as /proc or network shares. Paths that do not exist on your server produce a warning; remove them from the command.

Files:        48213 new,     0 changed,     0 unmodified
Dirs:          6121 new,     0 changed,     0 unmodified
Added to the repository: 1.103 GiB (412.771 MiB stored)

processed 48213 files, 1.611 GiB in 1:12
snapshot 7d4e1a90 saved

List snapshots to confirm:

restic snapshots
ID        Time                 Host        Tags        Paths
------------------------------------------------------------------
7d4e1a90  2026-09-25 10:21:44  web01       scheduled   /etc
                                                       /home
                                                       ...

Later backups only upload changed data, so they usually take seconds or a few minutes.

Step 5 - Defining a retention policy

Without a policy, snapshots accumulate forever. restic forget decides which snapshots to keep, and --prune removes the data only they referenced. Preview a policy that keeps 7 daily, 4 weekly, 12 monthly and 2 yearly snapshots:

restic forget --tag scheduled --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --keep-yearly 2 --dry-run

The output lists, per host and path set, which snapshots would be kept and why. Snapshots are grouped by host and paths by default, so servers sharing a repository never delete each other's snapshots. Filtering by --tag scheduled also keeps any manual snapshot you tag differently, such as one taken before an upgrade.

Pruning rewrites pack files, which on object storage means downloading and uploading data. --max-unused lets restic leave some unused space in exchange for much less rewriting; 5% is a reasonable value for cloud storage.

Step 6 - Automating backups with a systemd timer

A oneshot systemd service can run several commands in order and stops at the first failure, so no wrapper script is needed. The service below backs up, applies retention, and reads back a random 2.5% of the data to detect corruption early.

Create the service:

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

[Service]
Type=oneshot
EnvironmentFile=/etc/restic/restic.env
Environment=RESTIC_CACHE_DIR=/var/cache/restic
CacheDirectory=restic
Nice=10
IOSchedulingClass=idle
ExecStart=/usr/bin/restic backup --exclude-file /etc/restic/excludes --exclude-caches --one-file-system --tag scheduled /etc /home /root /opt /var/www
ExecStart=/usr/bin/restic forget --tag scheduled --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --keep-yearly 2 --prune --max-unused 5%
ExecStart=/usr/bin/restic check --read-data-subset 2.5%

CacheDirectory=restic makes systemd create /var/cache/restic, and restic stores its local metadata cache there, which speeds up every run. Nice and IOSchedulingClass=idle keep the backup from competing with your applications.

Create the timer:

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

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

[Install]
WantedBy=timers.target

Persistent=true runs a missed backup at the next boot if the server was off at 03:00.

Enable the timer and run the service once by hand to test it:

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

The start command returns when all three steps finish. Check the result:

systemctl status restic-backup.service --no-pager
journalctl -u restic-backup.service -n 20 --no-pager
restic[20411]: snapshot a2c93f11 saved
restic[20433]: Applying Policy: keep 7 daily, 4 weekly, 12 monthly, 2 yearly snapshots
restic[20459]: no errors were found
systemd[1]: restic-backup.service: Deactivated successfully.

Confirm the next scheduled run:

systemctl list-timers restic-backup.timer

If the upload saturates your uplink, add --limit-upload 20480 (in KiB/s, here 20 MiB/s) to the backup line.

Step 7 - Adding an append-only offsite copy with rest-server

The S3 credentials on the client can delete the bucket's contents, so ransomware or an attacker with root on the client could destroy your backups. A second copy on a restic REST server in append-only mode protects against this: clients can add snapshots but cannot delete or modify existing data.

On the backup server

Download the latest rest-server release from GitHub and install the binary:

sudo apt install restic jq apache2-utils
RS_VERSION=$(curl -fsSL https://api.github.com/repos/restic/rest-server/releases/latest | jq -r .tag_name | sed 's/^v//')
cd /tmp
curl -fLO "https://github.com/restic/rest-server/releases/download/v${RS_VERSION}/rest-server_${RS_VERSION}_linux_amd64.tar.gz"
tar -xzf "rest-server_${RS_VERSION}_linux_amd64.tar.gz"
sudo install -m 0755 "rest-server_${RS_VERSION}_linux_amd64/rest-server" /usr/local/bin/rest-server

Create a system user and the data directory, then add a bcrypt-hashed login for the client. Use the client's host name as the user name:

sudo useradd --system --home-dir /srv/restic --shell /usr/sbin/nologin restic
sudo install -d -o restic -g restic -m 0700 /srv/restic
sudo htpasswd -B -c /srv/restic/.htpasswd web01
sudo chown restic:restic /srv/restic/.htpasswd
sudo chmod 600 /srv/restic/.htpasswd

Create the service:

sudo nano /etc/systemd/system/rest-server.service
[Unit]
Description=Restic REST server
After=network.target

[Service]
Type=simple
User=restic
Group=restic
ExecStart=/usr/local/bin/rest-server --path /srv/restic --listen 127.0.0.1:8000 --htpasswd-file /srv/restic/.htpasswd --append-only --private-repos
Restart=on-failure
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/srv/restic

[Install]
WantedBy=multi-user.target

--private-repos restricts each user to repositories under a path matching their user name, so web01 can only use /web01/. Start it:

sudo systemctl daemon-reload
sudo systemctl enable --now rest-server

Publish it through Nginx with TLS. Create the site:

sudo nano /etc/nginx/sites-available/rest-server
server {
    listen 80;
    listen [::]:80;
    server_name backup.your_domain;

    client_max_body_size 0;
    proxy_request_buffering off;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
    }
}

client_max_body_size 0 removes the upload size limit, since restic uploads pack files of several megabytes. Enable it and request a certificate:

sudo ln -s /etc/nginx/sites-available/rest-server /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d backup.your_domain

On the client

Store the REST repository settings in a second file:

nano /etc/restic/offsite.env
RESTIC_REPOSITORY=rest:https://web01:[email protected]_domain/web01/
RESTIC_PASSWORD_FILE=/etc/restic/password
chmod 600 /etc/restic/offsite.env

Initialize it with the same chunker parameters as the primary repository. This keeps deduplication working between them, so copies only transfer new data:

set -a; source /etc/restic/offsite.env; set +a
restic init --from-repo "s3:https://s3.your_provider.com/your_bucket/your_hostname" \
  --from-password-file /etc/restic/password --copy-chunker-params

restic copy runs against the destination repository and reads from --from-repo. The S3 credentials are still needed for reading, so load both files in the service. Add a copy step at the end of /etc/systemd/system/restic-backup.service:

ExecStart=/bin/sh -c 'set -a; . /etc/restic/offsite.env; exec /usr/bin/restic copy --from-repo "s3:https://s3.your_provider.com/your_bucket/your_hostname" --from-password-file /etc/restic/password --tag scheduled'

Reload systemd, run the service again and list the snapshots in the offsite repository:

systemctl daemon-reload
systemctl start restic-backup.service
restic snapshots

The same snapshot IDs as in the primary repository appear. Now confirm append-only mode works by trying to delete one:

restic forget your_snapshot_id

The server refuses the deletion. Retention for the offsite repository is applied on the backup server itself, where the attacker's credentials do not reach, for example once a month:

sudo -u restic restic --no-cache -r /srv/restic/web01 forget --keep-daily 14 --keep-monthly 12 --prune

This asks for the repository password, which you type from your password manager, so it never needs to be stored on the backup server.

Step 8 - Restoring files

A backup you have never restored is only a hope. Back on the client, in the root shell with /etc/restic/restic.env loaded, list the files in the latest snapshot:

restic ls latest /etc/nginx

Restore a directory to a scratch location and compare it with the live copy:

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

To recover a whole server, install restic on a fresh machine, recreate /etc/restic/restic.env and the password file, and run restic restore latest --target /.

Troubleshooting

repository is already locked. A previous run crashed and left a stale lock. Make sure no restic process is running (pgrep -a restic), then run restic unlock.

Fatal: unable to open config file or wrong password. The environment file was not loaded or points to the wrong repository or password. Print the variables with env | grep -E 'RESTIC|AWS' in your shell.

The copy step fails with 403 on the REST server. The user name in the URL must match the first path component when --private-repos is enabled, and the password must match the .htpasswd entry.

Prune is slow and downloads a lot. Increase --max-unused, or move forget --prune into a separate weekly timer and keep only backup and check in the daily run.

Conclusion

Your server now backs up daily to S3-compatible storage with encryption, a retention policy and partial integrity checks, and every snapshot is copied to an append-only REST server that the client cannot erase. Consider adding OnFailure= to the service to trigger an alert unit, running a full restic check --read-data monthly, and practicing a full restore on a test VPS.