HOWTOIntermediate

Encrypted Offsite Backups with Restic, systemd Timers, and S3-Compatible Storage

Set up restic to produce deduplicated, client-side encrypted backups to Backblaze B2 or any S3 bucket, automate them with systemd, prune old snapshots, and prove restores actually work.

Dylan H.

Tutorials

November 9, 2026
5 min read
Encrypted Offsite Backups with Restic, systemd Timers, and S3-Compatible Storage

Introduction

Backups that are unencrypted, unverified, or writable by the machine they protect are the ones that fail during a ransomware event. Restic is a single-binary backup tool that encrypts everything client-side (AES-256 with Poly1305-AES authentication), deduplicates data into content-addressed chunks, and supports many backends including S3, Backblaze B2, SFTP, and local disks.

In this guide you will:

  • Install restic and initialise an encrypted repository in an S3-compatible bucket
  • Back up selected paths with sensible excludes
  • Automate backups with a systemd service and timer
  • Apply a retention policy and prune old data
  • Verify integrity and perform a real test restore

This pairs well with a broader plan such as the low-bandwidth strategies covered in our rural backup guide; restic's deduplication makes incremental uploads small, which matters on slow links.

Prerequisites

  • A Debian/Ubuntu or RHEL-family host (commands below use apt; adapt as needed)
  • A bucket and an application key scoped to only that bucket. Do not use your account master key.
  • Strongly recommended: enable bucket versioning or Object Lock on the bucket so a compromised host cannot silently destroy history
  • A long random passphrase for the repository. If you lose it, the data is unrecoverable. Store it in your password manager and a sealed offline copy.

Step 1: Install Restic

Distribution packages can lag. Check the version, and if it is old, use the official release binary.

sudo apt update && sudo apt install -y restic
restic version
 
# Self-update to the latest release if the packaged one is old
sudo restic self-update

Step 2: Store Credentials Securely

Keep secrets in a root-only environment file rather than in scripts or shell history.

sudo install -d -m 0700 /etc/restic
sudo tee /etc/restic/env >/dev/null <<'EOF'
RESTIC_REPOSITORY=s3:https://s3.us-west-004.backblazeb2.com/my-backup-bucket/host01
RESTIC_PASSWORD_FILE=/etc/restic/password
AWS_ACCESS_KEY_ID=REPLACE_WITH_KEY_ID
AWS_SECRET_ACCESS_KEY=REPLACE_WITH_APP_KEY
EOF
sudo chmod 0600 /etc/restic/env
 
# Generate a strong passphrase, then SAVE IT in your password manager
openssl rand -base64 36 | sudo tee /etc/restic/password >/dev/null
sudo chmod 0600 /etc/restic/password
sudo cat /etc/restic/password

Adjust the endpoint for your provider. For AWS S3 use s3:s3.amazonaws.com/bucket/path.

Step 3: Initialise the Repository

sudo bash -c 'set -a; . /etc/restic/env; set +a; restic init'

Restic prints created restic repository ... at s3:.... Using one sub-path per host (/host01) keeps repositories separate and simplifies retention.

Step 4: Define What to Back Up

Create an include list and an exclude list so the job is explicit and reviewable.

sudo tee /etc/restic/includes >/dev/null <<'EOF'
/etc
/home
/var/lib/docker/volumes
/srv
EOF
 
sudo tee /etc/restic/excludes >/dev/null <<'EOF'
/home/*/.cache
/home/*/node_modules
/var/lib/docker/volumes/*/_data/tmp
*.tmp
*.log
EOF

For databases, dump them first (for example pg_dump or mysqldump) into a directory that is in the include list; copying live database files gives inconsistent backups.

Step 5: Run a First Backup

sudo bash -c 'set -a; . /etc/restic/env; set +a;
  restic backup --files-from /etc/restic/includes \
                --exclude-file /etc/restic/excludes \
                --one-file-system --tag scheduled --verbose'

The first run uploads everything; later runs only send changed chunks.

Step 6: Automate with systemd

Create a service that backs up, then applies retention.

sudo tee /etc/systemd/system/restic-backup.service >/dev/null <<'EOF'
[Unit]
Description=Restic encrypted backup
Wants=network-online.target
After=network-online.target
 
[Service]
Type=oneshot
EnvironmentFile=/etc/restic/env
Nice=10
IOSchedulingClass=idle
ExecStart=/usr/bin/restic backup --files-from /etc/restic/includes --exclude-file /etc/restic/excludes --one-file-system --tag scheduled
ExecStart=/usr/bin/restic forget --tag scheduled --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --keep-yearly 2 --prune
ExecStart=/usr/bin/restic check --read-data-subset=5%
EOF
 
sudo tee /etc/systemd/system/restic-backup.timer >/dev/null <<'EOF'
[Unit]
Description=Nightly restic backup
 
[Timer]
OnCalendar=*-*-* 02:30:00
RandomizedDelaySec=15min
Persistent=true
 
[Install]
WantedBy=timers.target
EOF
 
sudo systemctl daemon-reload
sudo systemctl enable --now restic-backup.timer

Persistent=true runs a missed job after the machine boots, which helps laptops and VMs that are not always on. The check --read-data-subset=5% step downloads a random sample of data each night, so the whole repository is gradually validated over time.

Step 7: Alert on Failure

A silent backup failure is worse than none. Add an OnFailure= hook to notify you.

sudo mkdir -p /etc/systemd/system/restic-backup.service.d
sudo tee /etc/systemd/system/restic-backup.service.d/notify.conf >/dev/null <<'EOF'
[Unit]
OnFailure=restic-notify@%n.service
EOF
 
sudo tee /etc/systemd/system/restic-notify@.service >/dev/null <<'EOF'
[Unit]
Description=Notify on failure of %i
 
[Service]
Type=oneshot
ExecStart=/usr/bin/curl -fsS -m 10 -d "restic backup failed on %H (%i)" https://ntfy.example.com/backups
EOF
sudo systemctl daemon-reload

Replace the URL with your own ntfy, webhook, or healthcheck endpoint. A dead-man's-switch service (pinging on success) also catches the case where the timer never fires.

Verification and Testing

Confirm the timer and snapshots

systemctl list-timers restic-backup.timer
sudo systemctl start restic-backup.service
journalctl -u restic-backup.service -n 50 --no-pager
 
sudo bash -c 'set -a; . /etc/restic/env; set +a; restic snapshots'
sudo bash -c 'set -a; . /etc/restic/env; set +a; restic stats latest'

Test a real restore

A backup is only proven when you restore it. Do this on a schedule, ideally onto a different machine using only the passphrase and bucket credentials.

sudo bash -c 'set -a; . /etc/restic/env; set +a;
  restic restore latest --target /tmp/restore-test --include /etc/hostname'
cat /tmp/restore-test/etc/hostname
 
# Or browse the repository like a filesystem
sudo bash -c 'set -a; . /etc/restic/env; set +a; mkdir -p /mnt/restic && restic mount /mnt/restic'

Full integrity check

sudo bash -c 'set -a; . /etc/restic/env; set +a; restic check --read-data'

This downloads all data and can incur egress fees, so run it occasionally rather than nightly.

Hardening Notes

  • Limit blast radius: a compromised host holds credentials that can delete the bucket contents. Use bucket Object Lock/versioning, or an application key without deleteFiles, and run forget --prune from a separate trusted machine.
  • Separate credentials per host so one breach does not expose every repository.
  • Keep the passphrase off the host being backed up where possible, or at minimum in a root-only file as shown.
  • Follow the 3-2-1 rule: restic offsite is one copy; keep a local copy too (restic copy can replicate between repositories).

Troubleshooting

Fatal: unable to open config file ... Is there a repository at the following location? The repository path or endpoint is wrong, or the repo was never initialised. Check RESTIC_REPOSITORY and run restic init.

Fatal: wrong password or no key found The passphrase file does not match the repository. Confirm /etc/restic/password has no trailing whitespace added by an editor, and that you are pointing at the right repo path.

unable to create lock in backend: repository is already locked A previous run was interrupted. Confirm no restic process is running, then clear stale locks with restic unlock.

403 or Access Denied from the bucket The application key is scoped to a different bucket, lacks list/write capabilities, or the endpoint region is wrong. Recreate the key and verify the URL.

Backups are slow or saturate the link Limit bandwidth with --limit-upload 2000 (KiB/s) in the ExecStart line, and schedule the job off-hours.

Timer never fires Run systemctl list-timers --all and systemctl status restic-backup.timer. Make sure it was enabled with enable --now and the unit files have no typos (systemd-analyze verify /etc/systemd/system/restic-backup.*).

forget --prune takes a long time Pruning rewrites packs. Run it weekly instead of nightly by moving it into a separate service and timer.

Summary

You now have an encrypted, deduplicated, automated offsite backup pipeline: a restic repository in S3-compatible storage, credentials locked down under /etc/restic, a systemd timer with retention and rolling integrity checks, failure alerting, and a documented restore procedure. The most important habit is to schedule regular restore tests and protect the repository passphrase and bucket from deletion. A backup you have never restored is only a hope.