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
sudoprivileges. 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_domainpointing 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
WarningThe repository password is the encryption key for your backups. If you lose it, the data cannot be recovered by anyone. Store a copy in your password manager now.
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.
NoteRestic backs up files, not live databases. For MySQL or PostgreSQL, write a dump to a directory such as
/var/backups/dbbefore the backup (for example withmysqldumporpg_dump) and include that directory.
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.
