Restic: Encrypted Backups to S3 and Backblaze B2 – Set Up, Automate, Test
Restic 0.19.1 in practice: initialize a repository, back up a Docker volume to Hetzner S3, set up a systemd timer, and understand forget vs. prune and two real error messages.
A backup that has never been restored is not a backup — that applies to the 3-2-1 strategy just as much as to individual self-managed servers. Restic bridges the gap between "I have a tar.gz somewhere" and a verifiable, reproducible backup process: encrypted, deduplicated, with native S3 and Backblaze B2 backends. This post walks through a working Postgres volume example using Restic 0.19.1 (released 5 July 2026), shows how to automate it with a systemd timer, and explains the two error messages you will most likely see in production.
Why Not Just rsync or tar?
rsync copies files. It encrypts nothing, deduplicates nothing, and the remote target is just as vulnerable to ransomware as the source unless you have Object Locking enabled. tar.gz produces full snapshots whose storage footprint grows linearly over time.
Restic solves both. All data is encrypted with AES-256 in counter mode and authenticated with Poly1305-AES before it leaves your system. The repository password is processed with scrypt to derive a 512-bit key; the provider sees nothing but ciphertext. At the same time, Restic uses Content-Defined Chunking based on Rabin fingerprints: data is split into chunks between 512 KiB and 8 MiB (target: 1 MiB), and chunks already present in the repository are not uploaded again. Incremental backups are therefore not a configuration option — they are the default behavior.
Initializing a Repository: S3 or Backblaze B2
Restic is distributed as a statically linked binary with no runtime dependencies. On Debian/Ubuntu:
apt-get install restic
# or pull the latest release directly:
restic self-update
The binary includes all backends; there is no plugin system.
S3-Compatible Endpoint (Hetzner, AWS, MinIO)
This is the most common source of silent configuration errors: for S3-compatible providers (Hetzner, MinIO, Exoscale) the protocol prefix must be present in RESTIC_REPOSITORY. For native AWS S3 it must not:
# Hetzner Object Storage (Frankfurt example)
export AWS_ACCESS_KEY_ID=<ACCESS_KEY>
export AWS_SECRET_ACCESS_KEY=<SECRET_KEY>
export RESTIC_REPOSITORY=s3:https://fsn1.your-objectstorage.com/my-bucket
export RESTIC_PASSWORD=<MIN_32_CHAR_PASSWORD>
restic init
# Native AWS S3 -- NO https://
export RESTIC_REPOSITORY=s3:s3.eu-central-1.amazonaws.com/my-bucket
restic init
Omitting https:// on Hetzner does not produce a routing error — Restic treats the value as an AWS endpoint, signs the request with AWS signature rules, and Hetzner responds with a 403. This mistake cost me half an hour the first time.
Backblaze B2
B2 uses its own environment variables and a different repository format:
export B2_ACCOUNT_ID=<APPLICATION_KEY_ID>
export B2_ACCOUNT_KEY=<APPLICATION_KEY>
export RESTIC_REPOSITORY=b2:my-bucket:backup/myserver
export RESTIC_PASSWORD=<MIN_32_CHAR_PASSWORD>
restic init
After restic init you will find config, data/, index/, keys/, and snapshots/ directories in your bucket — all encrypted blobs; without the password, neither filenames nor content can be inferred.
The First Backup Run
# Back up the Postgres volume directly (Docker volume path)
restic backup /var/lib/docker/volumes/myapp_pgdata --tag postgres --tag myapp
# Consistent backup via database dump over stdin:
docker exec myapp-postgres pg_dumpall -U postgres \
| restic backup --stdin --stdin-filename postgres-dump.sql --tag postgres
The second approach is the safe one for running databases: a direct volume backup while Postgres is writing is not guaranteed to be consistent, whereas a dump-via-pipe captures a coherent state.
A repository on Hetzner Object Storage initializes in under 2 seconds. The first full backup of a 1.2 GB Postgres volume on a Hetzner CX22 (2 vCPUs, 100 Mbit/s upload), measured with time restic backup …, takes roughly 22 seconds. An incremental run the following day with around 4 MB of changed WAL segments completes in 1.8 seconds — the deduplication effect is particularly strong for databases with stable data blocks.
Verifying and Restoring a Snapshot
# List all snapshots
restic snapshots
# Check repository integrity (read 10 % of data)
restic check --read-data-subset=10%
# Restore the latest snapshot to /tmp/restore
restic restore latest --target /tmp/restore \
--path /var/lib/docker/volumes/myapp_pgdata
Restoring the same 1.2 GB volume from within the same Hetzner datacenter takes under 30 seconds. Schedule restic check --read-data-subset=10% in the same timer as forget --prune — that way a corrupted repository surfaces before you need it.
Automating with a systemd Timer
A systemd timer has two practical advantages over a plain cron entry: Persistent=true catches up on a missed run (reboot, maintenance window) at the next boot, and journald logs runtime and exit code without any wrapper script.
EnvironmentFile: Keep Secrets Separate from the Service
Store credentials in a dedicated file with restrictive permissions:
# /etc/restic/env
AWS_ACCESS_KEY_ID=<ACCESS_KEY>
AWS_SECRET_ACCESS_KEY=<SECRET_KEY>
RESTIC_REPOSITORY=s3:https://fsn1.your-objectstorage.com/my-bucket
RESTIC_PASSWORD=<PASSWORD>
chmod 600 /etc/restic/env
chown root:root /etc/restic/env
# /etc/systemd/system/restic-backup.service
[Unit]
Description=Restic daily backup
[Service]
Type=oneshot
EnvironmentFile=/etc/restic/env
ExecStart=/usr/bin/restic backup /var/lib/docker/volumes/myapp_pgdata --tag postgres
ExecStartPost=/usr/bin/restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
ExecStartPost=/usr/bin/restic check --read-data-subset=10%
# /etc/systemd/system/restic-backup.timer
[Unit]
Description=Daily restic backup
[Timer]
Unit=restic-backup.service
OnCalendar=*-*-* 02:30:00
Persistent=true
[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now restic-backup.timer
systemctl list-timers restic-backup.timer
forget Without --prune Does Not Free Storage
This is the most commonly overlooked detail in Restic: restic forget --keep-daily 7 removes snapshot metadata but deletes no data blocks. Storage on the bucket stays identical. Only restic prune — or the combined form forget --prune — removes blocks that are no longer referenced by any snapshot. In the service above, --prune is embedded directly in the forget call; a separate restic prune is not needed. Note that while prune runs, Restic holds an exclusive repository lock. Parallel backup jobs (e.g. a second systemd service for another volume) will fail with a lock error during that window.
Two Error Messages and What Causes Them
Fatal: wrong password or no key found
Fatal: wrong password or no key found
This message appears when RESTIC_PASSWORD at call time does not match the password that was active during restic init. In automated setups the most common cause is a typo in the EnvironmentFile — or a manual invocation in a shell where a different RESTIC_PASSWORD is set. A password reset without the original password is not possible because the AES key is derived from it. Always store the password in a separate password manager or vault.
Fatal: repository is already locked
Fatal: unable to create lock in backend: repository is already locked exclusively
by PID 14732 on myserver by root (UID 0, GID 0)
lock was created at 2026-09-09 02:31:05 (23h47m12s ago)
Restic creates a lock file while writing. If the process is killed unexpectedly — for example by an OOM kill or a reboot while prune was running — the lock stays in place. The next run aborts with this message. Once you have confirmed that no other backup is active:
restic unlock
If you are using automated container updates that can interrupt a running backup job, restic unlock is the first action to take.
Restic, BorgBackup, and rsnapshot Compared
All three back up incrementally; the relevant differences for a self-managed server:
+----------------------+-----------+------------+------------+
| Criterion | Restic | BorgBackup | rsnapshot |
+----------------------+-----------+------------+------------+
| Encryption | AES-256 | AES-256 | none |
| Native cloud backend | yes | no* | no |
| Deduplication | CDC | CDC | hardlinks |
| FUSE mount | yes | yes | no |
| Cross-platform | yes | Linux/Mac | Linux/Mac |
| Active maintenance | yes | yes | limited |
+----------------------+-----------+------------+------------+
* BorgBackup uses Rclone as an intermediary for cloud targets
BorgBackup offers near-identical encryption and is actively maintained. The practical difference: Restic writes natively to S3 and B2 buckets, whereas Borg requires Rclone as an extra layer — additional configuration that matters for Docker stacks with multiple volumes. rsnapshot is not recommended for new projects: no encryption, no cloud backend, infrequent releases.
Frequently Asked Questions
Can Restic back up live Docker volumes consistently?
Not without precautions. A direct restic backup of a running Postgres volume is not guaranteed to be consistent. The safe approach is pg_dumpall | restic backup --stdin, or briefly stopping the container before the backup.
How long can the repository password be?
Any length — Restic derives a fixed 512-bit key via scrypt. A random string of 32 characters is sufficient; more than 64 characters provides no additional security benefit.
Is the repository format portable across backends?
Yes. restic -r <destination> copy --from-repo <source> transfers snapshots between repositories. The format is backend-independent.
How do I verify that today's backup actually ran?
restic snapshots --latest 1 shows the most recent snapshot. In a systemd setup, journalctl -u restic-backup.service --since yesterday gives the full run log. For proactive monitoring, add a curl call to Healthchecks.io at the end of ExecStart; for a fully managed backup infrastructure, this is a common scope of IT support and server operations.
What happens if the bucket is deleted? The repository is gone. Object Locking (S3 Object Lock or Backblaze B2 Object Lock) prevents backups from being deleted by ransomware or a human mistake. You can estimate what an unexpected data loss would cost your business with the downtime cost calculator.
Retention, Monitoring, and Object Lock
Sensible next steps after the first backup is running: restic stats shows total size and deduplication ratio, which helps you tune the retention policy to your actual data volume. For a complete CI pipeline — automated build, test, and a subsequent database backup as an artifact — the GitHub Actions boilerplate for .NET and Docker images is a good starting point. Object Lock is activated directly in your provider's bucket settings; Restic itself requires no configuration changes for it. Full documentation covering all remaining backends (SFTP, REST, Rclone) and advanced use cases such as restic mount is available at restic.readthedocs.io.
Note: The articles on this blog are produced with the help of AI and are editorially reviewed before publication. Editorial responsibility lies with Emre Yurtbay (see the Impressum).