Restic backups: encrypted and off-site
Off-site backups with Restic: set up encrypted, restore snapshots, prune with retention and automate via a systemd timer.
Table of contents
A snapshot at the provider is not a backup: it sits on the same infrastructure and is gone too if your account is suspended, the data center burns down, or you accidentally delete the wrong project. Restic turns it into a real backup – encrypted, deduplicated and in a place outside your server.
What are we building?
By the end, Restic backs up your important data – app volumes, configurations, database dumps – end-to-end encrypted and deduplicated into a repository, ideally off-site (a second server or storage). You can restore individual files or whole snapshots at any time, automatically thin out old backups by a retention policy, and check integrity. Finally, everything runs automatically via a systemd timer and alerts you if a run fails to happen.
Tested with Restic 0.18 (the version from the Debian 13 package sources); upstream already has 0.19.x – how to update is in step 1. Restic always encrypts – there is no unencrypted repository.
Snapshot ≠ backup
Prerequisites
- A server (Debian 13) with the data you want to back up – e.g. the volumes and Compose files of your Docker apps.
- An off-site target for the backups. Restic speaks, among others:
- SFTP – a second server or a storage box (easiest to start with),
- S3-compatible – e.g. an object-storage bucket,
- rclone – as a bridge to Backblaze B2, Google Drive and dozens more.
- Basic familiarity with the shell.
VPS 1000 G12
4 vCores · 8 GB RAM · 256 GB NVMe
from €10.36/month
A small second VPS at a different location is a cheap, dedicated backup target via SFTP.
💶 5 € voucher for new netcup customers:
36nc17844976032
(new customers only, no domains)
Step by step
Step 1: Install Restic
Restic is in the package sources and is updated with the system:
sudo apt update
sudo apt install -y resticCheck the version:
restic versionrestic 0.18.0 compiled with go1.24.4 on linux/amd64A newer version if needed
/usr/local/bin (that takes precedence over /usr/bin). The built-in command
restic self-update does not work with the Debian package version – the package is
built without that feature (unknown command "self-update"). For most people the
package version is entirely enough.Step 2: Create the encrypted repository
A Restic repository is the encrypted storage location of your backups. It’s protected with a password – and this password does not belong in a script and not next to the repository. Put it in a file only root may read:
sudo install -d -m 700 /etc/restic
openssl rand -base64 32 | sudo tee /etc/restic/password > /dev/null
sudo chmod 600 /etc/restic/passwordThe openssl command generates a long random password and stores it under
/etc/restic/password (the install command before it creates the directory, the
chmod after it protects the file). Tell Restic via environment variables where the
repository is and where the password is – here first an off-site target via SFTP:
export RESTIC_REPOSITORY="sftp:BACKUP_USER@YOUR_BACKUP_HOST:/backups/YOUR_SERVER"
export RESTIC_PASSWORD_FILE="/etc/restic/password"Replace BACKUP_USER, YOUR_BACKUP_HOST and the path with your real target. If you
want to practice locally first, use a folder instead:
export RESTIC_REPOSITORY="/srv/restic-repo". The commands afterwards are
identical – only the repository address differs.
You use other off-site targets via the same variable, only with a different prefix:
- S3-compatible:
s3:https://ENDPOINT/BUCKET– plus the credentials asAWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEYin the environment (or in the environment file from step 7). - rclone:
rclone:REMOTE:path– the bridge to Backblaze B2, Google Drive and many more, once you’ve set up the rclone remote.
No matter which backend: encryption, deduplication and all the following commands stay the same.
Now initialize the repository (the example output shows the local practice repository –
with the SFTP target, your sftp: address appears there instead):
restic initcreated restic repository d29613209a at /srv/restic-repo
Please note that knowledge of your password is required to access
the repository. Losing your password means that your data is
irrecoverably lost.Password lost = data lost
/etc/restic/password additionally in
a password manager – separate from the server, otherwise it’s no use to you if the
server is lost.Step 3: The first backup
Back up the directory with your data. The --tag helps with filtering later:
restic backup /root/backup-demo --tag demono parent snapshot found, will read all files
Files: 3 new, 0 changed, 0 unmodified
Dirs: 2 new, 0 changed, 0 unmodified
Added to the repository: 2.002 MiB (2.001 MiB stored)
processed 3 files, 2.000 MiB in 0:00
snapshot f16c6ffd savedOn the second run, Restic’s strength shows: it only backs up the changes (deduplication). If a file changes and you back up again:
using parent snapshot f16c6ffd
Files: 0 new, 1 changed, 2 unmodified
Added to the repository: 1.820 KiB (1.121 KiB stored)
processed 3 files, 2.000 MiB in 0:00
snapshot 0bb5e679 savedInstead of 2 MiB again, only 1.1 KiB land in the repository – just the changed part.
What belongs in the backup? For Docker apps that’s three things – recipe, payload, DB:
- Compose files (your “recipe”) and
.env: small but indispensable for rebuilding the stack. - Bind mounts (
./data:/…) live directly in the project folder – you back those up right along. Named volumes you’ll find under/var/lib/docker/volumes/<name>/_data; include the path. - Databases never as a live file – a backup mid-write is potentially unusable.
Instead create a consistent dump (
pg_dump/mariadb-dump) and back that up (see step 7).
You exclude the unnecessary:
restic backup /opt --tag apps --exclude='*.log' --exclude='**/cache/**'Step 4: Restore
A backup is only as good as its restore. First list the snapshots:
restic snapshotsID Time Host Tags Paths Size
---------------------------------------------------------------------------------------------
f16c6ffd 2026-07-16 20:52:35 v2200000000000000000 demo /root/backup-demo 2.000 MiB
0bb5e679 2026-07-16 20:52:36 v2200000000000000000 demo /root/backup-demo 2.000 MiB
---------------------------------------------------------------------------------------------
2 snapshotsRestore a complete snapshot to a target directory (use latest for the newest or a
specific ID):
restic restore latest --target /root/restore-testrestoring snapshot 0bb5e679 of [/root/backup-demo] ... to /root/restore-test
Summary: Restored 5 files/dirs (2.000 MiB) in 0:00If you only need one file, there are two ways – restore that specific path:
restic restore latest --target /tmp/wiederher --include /root/backup-demo/config.yaml… or stream the file directly to standard output, without any detour:
restic dump latest /root/backup-demo/config.yaml > /tmp/config.yamlTo browse, you can even mount the repository as a filesystem (restic mount /mnt/backup, requires FUSE) and copy out what you need in the file manager – handy when
you no longer know which snapshot the version you’re looking for is in.
Step 5: Check integrity
check verifies that the repository’s structure is intact:
restic checkload indexes
check all packs
check snapshots, trees and blobs
[0:00] 100.00% 2 / 2 snapshots
no errors were foundThat checks the metadata, but not the actual data. So regularly read part of the real data along with it – e.g. 10% per run, i.e. everything over the course of weeks:
restic check --read-data-subset=10%Step 6: Prune with a retention policy
Without pruning, the repository grows forever. forget applies a retention policy,
--prune frees the storage of the removed snapshots:
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --pruneApplying Policy: keep 7 daily, 4 weekly, 6 monthly snapshots
keep 2 snapshots:
2 snapshotsThis keeps the last 7 daily, 4 weekly and 6 monthly snapshots – a good default. The
tiers interlock: fine resolution for the recent past (one per day), coarser the further
back (one per week, then one per month). So, if in doubt, you can get close to the
desired state without the repository growing unbounded. Older snapshots drop off
automatically. --prune is the expensive part (it repacks data); on large
repositories you often run it only once or twice a week, not on every backup.
Step 7: Automate everything (systemd timer)
A backup you have to start by hand is one you forget. Put the credentials in an environment file (so the service knows them):
# /etc/restic/restic.env
RESTIC_REPOSITORY=sftp:BACKUP_USER@YOUR_BACKUP_HOST:/backups/YOUR_SERVER
RESTIC_PASSWORD_FILE=/etc/restic/passwordThen a wrapper script /usr/local/bin/restic-backup.sh – it first dumps the database
(PostgreSQL example), backs up, prunes and finally reports success to
Uptime Kuma (the push monitor as a “dead man’s
switch”):
#!/usr/bin/env bash
set -euo pipefail
# 0) Ensure the target directory for the dump (otherwise the first run aborts)
mkdir -p /opt/dumps
# 1) Dump the database consistently (never back up the live DB file)
docker exec YOUR_DB_CONTAINER pg_dumpall -U postgres > /opt/dumps/db.sql
# 2) Backup
restic backup /opt/apps /opt/dumps --tag auto
# 3) Prune (retention)
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
# 4) Report success to Uptime Kuma
curl -fsS "https://status.YOUR_DOMAIN/api/push/YOUR_TOKEN?status=up&msg=restic+OK" > /dev/nullsudo chmod 700 /usr/local/bin/restic-backup.shThe service loads the environment file and runs the script –
/etc/systemd/system/restic-backup.service:
[Service]
Type=oneshot
EnvironmentFile=/etc/restic/restic.env
ExecStart=/usr/local/bin/restic-backup.shAnd the timer /etc/systemd/system/restic-backup.timer starts it nightly:
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.targetPersistent=true catches up a missed run (e.g. because the server was off at night) on
the next start. Enable it:
sudo systemctl daemon-reload
sudo systemctl enable --now restic-backup.timerCheck with systemctl list-timers restic-backup.timer when the next run is due.
Don't let backup time collide with the automatic reboot
04:00), the backup must be done before then. If the running backup run
(restic backup or the subsequent restic forget --prune) is cut off mid-operation by
the reboot, a stale lock remains in the repository (repository is already locked,
see below). That’s why the timer here starts at 02:30 – enough headroom for a larger
backup plus pruning. If you adjust either time, deliberately keep the gap large.Step 8: Rehearse the worst case once
A backup you’ve never restored is just a hope. So deliberately run through a complete restore once – ideally on a different or freshly set-up server, so you also need the credentials (repository address and password) under real conditions:
export RESTIC_REPOSITORY="sftp:BACKUP_USER@YOUR_BACKUP_HOST:/backups/YOUR_SERVER"
export RESTIC_PASSWORD_FILE="/etc/restic/password"
restic snapshots
restic restore latest --target /tmp/dr-testThen compare samples with the original (diff, file hashes) and boot the app from the
restored data as a test. Only once that works do you know your backup holds up in an
emergency – and that you’ll remember the procedure when it really counts.
When things go wrong
subprocess ssh: Host key verification failed with the SFTP target. Restic can’t
confirm the SSH host key of the backup server because it isn’t known yet. Connect
once manually (ssh BACKUP_USER@YOUR_BACKUP_HOST) and confirm the fingerprint – or
store it with ssh-keyscan YOUR_BACKUP_HOST >> ~/.ssh/known_hosts. After that Restic
runs through.
wrong password or repository does not exist. RESTIC_REPOSITORY or
RESTIC_PASSWORD_FILE isn’t set or points nowhere – so check the EnvironmentFile in
the systemd service. A manually set export only applies in the current shell.
repository is already locked. An aborted run left a lock. Check that really
no backup is running anymore, then restic unlock. Never unlock blindly while a run
is active in parallel.
The backup gets huge / backs up nonsense. Restrict it with
--exclude/--exclude-file (caches, logs, temporary files) and use
--one-file-system so Restic doesn’t dive into mounted foreign filesystems.
prune takes forever or was aborted. No reason to panic – prune is resumable and
the repository stays valid. Start it again and afterwards run restic check once.
Separate --prune from the daily backup on large repos (see step 6).
The timer runs, but a success message never arrives in Uptime Kuma. Look at the
last run with journalctl -u restic-backup.service. Usually the script aborts
before the curl call (e.g. the DB dump failed) – thanks to set -euo pipefail it
then stops cleanly instead of reporting a broken backup as success. So the missing push
isn’t a bug, but exactly the alarm signal you want.
Maintenance & backups
- Test restores regularly – that’s the single most important point. A backup you’ve never restored is not a backup but a hope. Schedule a test restore into an empty directory monthly.
- 3-2-1 rule: at least 3 copies, on 2 different media, of which 1 is off-site. Restic covers the off-site piece – but “off-site” really means a different provider/location, not the same data center.
- Monitor backups: without monitoring, you only notice a dead backup in an emergency. The push call from step 7 to Uptime Kuma raises the alarm when a run fails to happen.
- Check integrity: run
restic check --read-data-subsetregularly so silent bit rot is caught before you need the data. - Password/key management: keep the repository password separate from the server.
For individual clients you can assign additional passwords with
restic key addwithout sharing the main password.
Last updated: Jul 19, 2026
Send feedback: feedback@serverkueche.de
What's next?

Self-hosting Immich: your private photo backup behind Traefik
Set up Immich with Docker behind Traefik: the self-hosted alternative to Google Photos – with automatic phone backup, …

Self-hosting Paperless-ngx: the paperless office with OCR
Set up Paperless-ngx with Docker behind Traefik: archive documents searchably via OCR – with full-text search, tags and …

Self-hosting Nextcloud: your own cloud behind Traefik
Set up Nextcloud with Docker behind Traefik: your own cloud for files, calendar and contacts – with MariaDB, Redis and …

Vaultwarden: your own password manager behind Traefik
Self-host Vaultwarden: a lean, Bitwarden-compatible password manager behind Traefik with HTTPS, an admin panel and an …
You might also like

Setting up Fail2ban: block brute-force attacks automatically
Fail2ban watches your logs and bans IPs after too many failed attempts – with a safe whitelist for your own address so …

CrowdSec: modern, collaborative intrusion prevention
Set up CrowdSec with the Traefik bouncer: detect attacks from the access logs, block attackers with a 403 and benefit …

unattended-upgrades: automatic security updates for Debian
Debian installs security updates automatically: set up unattended-upgrades, control the reboot, and verify it really …