Skip to Content

Encrypted Linux Backups with restic and systemd Timers

Set up automatic, encrypted, versioned server backups to a separate host, with retention, integrity checks and a tested restore.
October 28, 2026 by
Encrypted Linux Backups with restic and systemd Timers
JZ Web

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/password

Warning: 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/restic

SFTP 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
*.tmp

restic 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.target

What the important lines do:

  • --one-file-system keeps 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=strict mounts the whole filesystem read-only for this service. A backup only needs to read, so this limits the damage if something goes wrong. CacheDirectory=restic creates /var/cache/restic as the one writable location; it matches RESTIC_CACHE_DIR above. If your repository is a local path or mounted disk, add ReadWritePaths=<path>.
  • Persistent=true runs the job after boot if a scheduled run was missed, and RandomizedDelaySec spreads 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.target

restic 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.timer

Step 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/hostname

The 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/prune from a separate, well-secured machine. With append-only repositories use --keep-within rather 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 --failed or 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/env and the password file readable by root only (mode 600/700)
  • Nightly backup timer enabled, first run checked in the journal
  • Weekly forget --prune and check --read-data-subset timer 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.

Contact us

Sources

Hardening SSH on Ubuntu 24.04 with a Drop-In File
Disable passwords and root logins, verify the effective sshd settings and test safely without locking yourself out.