Server backups with Restic and S3: lower cost, incremental uploads
How to build encrypted, incremental server backups with Restic using Amazon S3 or Backblaze B2's S3-compatible API.
- #backup
- #restic
- #s3
- #backblaze
- #infrastructure
This article was written with the help of AI.
Backing up a server should not mean cloning whole disks, paying for the same bytes repeatedly, or hoping that a local rsync job will save the day. A pragmatic setup for Linux servers is Restic plus S3-compatible object storage: Amazon S3 when the infrastructure already lives in AWS, or Backblaze B2 when storage cost is the primary concern.
Restic handles the difficult client-side work: encryption, deduplication, snapshots, and restoration. The bucket provides durability and keeps the backup away from the server that may fail.
Why this combination works
The first run uploads every required data block. Later runs compare the current state with the parent snapshot, reuse data already in the repository, and upload only new content. Every run can create a snapshot, which lets you restore a directory as it existed at a specific point in time.
That changes the economics. A server may hold 200 GB, while only 2 GB of databases, uploads, or configuration change each day. Uploading 200 GB daily would be wasteful. Restic may still process a large amount of data locally because it must inspect files, but the transferred amount usually follows the change rate. Deduplication also works across snapshots and can help multiple hosts sharing one repository, provided that matches the team’s isolation policy.
There is another important benefit: Restic encrypts the repository before upload. The object-storage provider holds encrypted blobs; the repository password remains necessary to recover the data.
S3, Backblaze B2, and cost
Amazon S3 is a natural option when servers, permissions, and observability already live in AWS. The cost model is broader than storage per GB: requests, retained versions, transfer, and storage class all matter.
Backblaze B2 is often compelling for backups because of its storage pricing. For Restic, the project’s current documentation recommends using B2 through its S3-compatible API instead of the native B2 backend. Create an S3-compatible application key scoped to the backup bucket and use the endpoint for that bucket’s region.
With B2, add a lifecycle rule that keeps only the last version of each file. Restic’s S3 backend hides objects that are no longer needed; without a lifecycle rule, hidden versions can continue consuming space and undermine the expected savings.
No provider is always cheapest. Model at least:
- initial data size and monthly growth;
- actual daily change rate;
- required retention period;
- request and restore costs;
- data transfer out during a disaster-recovery scenario.
The best price is the total cost of a backup you can actually restore, not the lowest advertised cost per GB.
Configuration: keep credentials out of the script
Start with a dedicated bucket, such as my-server-restic. Grant its access key only the permissions needed for that bucket; do not use account-wide administrative credentials.
Store variables in a file readable only by the backup user, for example /etc/restic/env with 0600 permissions:
export RESTIC_REPOSITORY="s3:https://s3.<region>.backblazeb2.com/my-server-restic"
export RESTIC_PASSWORD_FILE="/etc/restic/password"
export AWS_ACCESS_KEY_ID="<bucket-s3-key>"
export AWS_SECRET_ACCESS_KEY="<bucket-s3-secret>"
export AWS_DEFAULT_REGION="<bucket-region>"
For Amazon S3, use the region endpoint in the repository location:
export RESTIC_REPOSITORY="s3:s3.us-east-1.amazonaws.com/my-server-restic"
For an S3-compatible provider, s3:https://endpoint/bucket is the explicit form. When needed, specify the region with AWS_DEFAULT_REGION or -o s3.region="...". Do not put secrets in crontab, a Git repository, or a unit file readable by every user.
Initialize the repository once:
source /etc/restic/env
restic init
Losing the repository password means losing the ability to restore the data. Keep it in a secret manager and design a recovery procedure that does not depend on a single person.
A useful daily backup
The example below avoids pseudo-filesystems and paths that should not be copied as ordinary files. Adapt the list to the server’s role. A web application, for example, usually needs configuration, uploads, and a consistent database dump — not an indiscriminate copy of /.
#!/usr/bin/env bash
set -euo pipefail
source /etc/restic/env
restic backup \
--tag server:app-01 \
--tag daily \
--exclude-file=/etc/restic/excludes.txt \
--one-file-system \
--skip-if-unchanged \
/etc /home /srv /var/www /var/backups
A starting excludes.txt may contain:
/proc
/sys
/dev
/run
/tmp
/var/cache
/var/tmp
/var/lib/docker/overlay2
--skip-if-unchanged prevents a new snapshot when nothing changed. It does not replace deduplication: deduplication already avoids retransferring existing data; the option simply avoids filling the history with identical snapshots.
For databases, first create a transactional or otherwise consistent dump in /var/backups, then include that dump. Copying an actively running database’s internal files is not automatically a consistent, restorable backup.
Retention, pruning, and verification
Backup without retention becomes accumulation; retention without pruning does not shrink the bucket. A straightforward policy can retain daily, weekly, monthly, and yearly snapshots:
restic forget --prune \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 12 \
--keep-yearly 3
Schedule regular verification:
restic check
restic snapshots
Most importantly, test a restore. A minimal exercise restores a snapshot into a temporary directory and validates an important file:
restic restore latest --target /tmp/restore-test
For production, rehearse recovery on another machine and document where credentials live, how to install Restic, how to choose a snapshot, and how to bring data and services back online.
Practical operations
Run the script with a systemd timer or cron, record its output, and alert on failure. Serialize executions per host to avoid unnecessary concurrency against the same repository. It can also be worthwhile to limit bandwidth and I/O when backups compete with production traffic.
The result is a simple flow: changed data reaches the bucket encrypted, repeated data is not sent again, old snapshots expire according to policy, and restoration becomes part of normal operations rather than a hope during an incident.