A backup you have never restored is a hope, not a backup. This guide is for small businesses and IT staff who run a Linux server (web, Odoo, file or mail) and want encrypted, versioned, automatically scheduled backups to a separate machine, using restic and systemd timers. It targets Ubuntu 24.04 LTS and Debian 13, and uses no extra tooling beyond what the distributions provide.
Why restic and systemd timers?
restic is a single-binary backup program. Every snapshot is encrypted and deduplicated before it leaves the server, and unchanged data is stored only once. That makes daily snapshots of a few hundred gigabytes affordable, and a stolen or misconfigured backup target does not expose your data. restic can write to a local path, SFTP, S3-compatible object storage and a REST server.
Instead of a cron job we use a systemd timer. Compared with cron it gives you logging in the journal, a clear failure state per run, resource and sandbox settings, and Persistent=true, which catches up a run that was missed while the machine was off.
Note: Ubuntu 24.04 ships restic 0.16.4 and Debian 13 ships restic 0.18.0 in their repositories. Everything below works with both, except --stdin-from-command in the database section, which requires restic 0.17.0 or newer. Install with sudo apt install restic.
Step 1: Prepare credentials and the configuration
restic needs a repository location and a repository password. We keep both outside the unit files, in root-only files.
sudo install -d -m 0700 /etc/restic
sudo sh -c 'umask 077; openssl rand -base64 32 > /etc/restic/password'
sudo cat /etc/restic/passwordWarning: Without this password the backup cannot be decrypted, and there is no recovery. Store a copy in your password manager and in a sealed printout, not only on the server you are backing up.
Now create the environment file. This example uses SFTP to a separate backup host; replace the placeholders.
# /etc/restic/env (chmod 600, owner root)
RESTIC_REPOSITORY=sftp:<backup-user>@<backup-host>:/srv/restic/<server-name>
RESTIC_PASSWORD_FILE=/etc/restic/password
RESTIC_CACHE_DIR=/var/cache/resticSFTP requires passwordless key login for root on the source server. Create a dedicated key (ssh-keygen -t ed25519 -f /root/.ssh/id_restic), install the public key on the backup host, and connect once manually with sudo ssh -i /root/.ssh/id_restic <backup-user>@<backup-host> so the host key is accepted. Then reference that key with an IdentityFile entry for the backup host in /root/.ssh/config; restic uses the SSH configuration file for SFTP connections. For S3-compatible storage you would instead set RESTIC_REPOSITORY=s3:<endpoint>/<bucket> plus AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in the same file.
Initialise the repository once:
sudo chmod 600 /etc/restic/env
sudo bash -c 'set -a; . /etc/restic/env; restic init'What to back up
Back up what you cannot recreate: configuration (/etc), application data and uploads, home directories and database dumps. Do not back up the operating system itself, caches or the backup repository. A starting point for /etc/restic/excludes.txt:
# /etc/restic/excludes.txt
/var/www/*/cache
/home/*/.cache
*.tmprestic ignores empty lines and lines beginning with # in exclude files. Adjust the paths to your applications.
Step 2: The backup service and timer
# /etc/systemd/system/restic-backup.service
[Unit]
Description=restic backup
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
EnvironmentFile=/etc/restic/env
CacheDirectory=restic
ExecStart=/usr/bin/restic backup --one-file-system --exclude-caches \
--exclude-file=/etc/restic/excludes.txt --tag nightly \
/etc /home /srv /var/www
Nice=10
IOSchedulingClass=idle
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only# /etc/systemd/system/restic-backup.timer
[Unit]
Description=Nightly restic backup
[Timer]
OnCalendar=*-*-* 02:30:00
RandomizedDelaySec=30min
Persistent=true
[Install]
WantedBy=timers.targetWhat the important lines do:
--one-file-systemkeeps restic from crossing into other mounts such as network shares or the backup target itself. If your data lives on several filesystems, list each path explicitly.ProtectSystem=strictmounts the whole filesystem read-only for this service. A backup only needs to read, so this limits the damage if something goes wrong.CacheDirectory=resticcreates/var/cache/resticas the one writable location; it matchesRESTIC_CACHE_DIRabove. If your repository is a local path or mounted disk, addReadWritePaths=<path>.Persistent=trueruns the job after boot if a scheduled run was missed, andRandomizedDelaySecspreads load when several servers share one backup host.
Exit code 3 means restic could not read some source files; it still creates a snapshot of everything else. We deliberately leave systemd treating that as a failure so that you notice it.
sudo systemctl daemon-reload
sudo systemctl enable --now restic-backup.timer
sudo systemctl start restic-backup.service
journalctl -u restic-backup.service -n 50 --no-pager
systemctl list-timers 'restic-*'Step 3: Retention and integrity checks
Without a retention policy the repository grows forever. forget removes snapshots according to a policy and --prune reclaims the space. Run it weekly, not nightly: pruning locks the repository, so backups cannot complete during it.
# /etc/systemd/system/restic-maintenance.service
[Unit]
Description=restic retention and integrity check
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
EnvironmentFile=/etc/restic/env
CacheDirectory=restic
ExecStart=/usr/bin/restic forget --keep-daily 7 --keep-weekly 5 --keep-monthly 12 --prune
ExecStart=/usr/bin/restic check --read-data-subset=5%
Nice=10
IOSchedulingClass=idle
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=read-only# /etc/systemd/system/restic-maintenance.timer
[Unit]
Description=Weekly restic maintenance
[Timer]
OnCalendar=Sun *-*-* 05:00:00
Persistent=true
[Install]
WantedBy=timers.targetrestic check verifies the repository structure; --read-data-subset=5% additionally downloads and verifies a random 5 percent of the pack files, so bit rot at the target is eventually caught without transferring everything each week. The percentage form does not guarantee that all data is covered over time. If you want full coverage, use --read-data-subset=n/t (for example group 1 of 5 in week one, group 2 of 5 in week two). Preview any new retention policy first with restic forget --dry-run <options>.
Warning: restic refuses an empty policy, but a policy that is too short deletes history permanently. Keep an eye on the first --dry-run output.
sudo systemctl daemon-reload
sudo systemctl enable --now restic-maintenance.timerStep 4: Test the restore
Do this right after the first backup and repeat it regularly, for example quarterly. Restore into a scratch directory, never over live data.
sudo bash -c 'set -a; . /etc/restic/env; restic snapshots'
sudo bash -c 'set -a; . /etc/restic/env; \
restic restore latest --target /root/restore-test --include /etc/hostname'
sudo cat /root/restore-test/etc/hostnameThe restored file appears below the target directory with its original path. To browse a whole snapshot, restic mount <mountpoint> exposes the repository as a read-only filesystem. Also practise the worst case: on a clean machine with only the repository location and the password, can you restore?
Databases
Copying live database files gives inconsistent backups. Create a dump instead and let restic store it. restic can read the output of a command and fails the backup if the command fails, which avoids silently stored empty dumps. This option was added in restic 0.17.0: it is available in Debian 13 (0.18.0), but not in the restic 0.16.4 package of Ubuntu 24.04.
restic backup --stdin-filename <db-name>.sql --stdin-from-command -- mysqldump --single-transaction <db-name>Use the matching dump tool for your database (pg_dump for PostgreSQL, mariadb-dump or mysqldump for MariaDB/MySQL) and handle its credentials via a root-only option file. The shell-pipe variant (mysqldump | restic backup --stdin) is discouraged in restic's documentation because a failing dump can still produce an empty backup. Note that sudo inside the unit does not work with NoNewPrivileges=true; run the dump as a user that has direct database access or run it in its own unit.
Protect the backups themselves
- The server holds the credentials to the repository. If an attacker takes over the server, they can delete the backups too. Reduce that risk with an append-only target (restic's documentation describes append-only repositories, for example with rest-server) or object-lock features of your storage provider, and run
forget/prunefrom a separate, well-secured machine. With append-only repositories use--keep-withinrather than count-based rules. - Keep one copy off-site and in a different failure domain than the server (different provider, different account).
- Watch for failures:
systemctl --failedor a monitoring check on the unit state, so a silently failing backup does not go unnoticed for months. restic also documents its exit codes (for example 11 for a locked repository and 12 for a wrong password, from restic 0.17.0/0.17.1) if you want to react to specific errors. - Backups of personal data fall under the GDPR. The encryption helps, but document retention periods and the backup location (EU hosting is easiest) in your records of processing. This is general information, not legal advice.
Checklist
- restic installed from the distribution repository; repository initialised and password stored in two safe places
/etc/restic/envand the password file readable by root only (mode 600/700)- Nightly backup timer enabled, first run checked in the journal
- Weekly
forget --pruneandcheck --read-data-subsettimer enabled; policy tested with--dry-run - Databases backed up via dump, not via live files
- Restore tested into a scratch directory, and the full-disaster procedure written down
- Copy in a separate failure domain, preferably append-only; failures monitored
Our take
Many small-business outages become disasters only because nobody ever tested the restore. We prefer a deliberately plain setup like this one, a few readable unit files that any administrator can inspect, over a complex backup suite. Whatever tool you choose, the regular restore test is the part that counts.
Sources
- restic documentation: Preparing a new repository
- restic documentation: Backing up
- restic documentation: Working with repositories
- restic documentation: Restoring from backup
- restic documentation: Removing backup snapshots
- restic documentation: Scripting (exit codes)
- systemd.timer(5)
- systemd.exec(5)
- Debian 13 package: restic
- Ubuntu 24.04 package: restic