> ## Content Index
> Fetch the complete content index at: https://blog.sudowl.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Restic Backups to S3 / MinIO — Encrypted, Deduplicated, Restore-Tested
- URL: https://blog.sudowl.com/restic-backup-s3-minio-encrypted/
- Published: 2026-04-27T14:00:00.000Z
- Updated: 2026-10-11T08:47:37.000Z
- Description: Real backup setup with Restic — encrypted offsite to S3 or self-hosted MinIO, dedup, retention, and the restore-drill that proves your backups actually work.
- Author: Lukas
- Tags: Linux, Ubuntu, Security

[rsync to a directory](/rsync-backup-linux/) is fine for "I have one server and a USB disk." The moment you have more than one box, want offsite, or need to restore a specific file from three weeks ago — Restic is the upgrade. Encrypted, deduplicated, content-addressable, with proper retention pruning.

I run this on every server I care about. Two `restic` commands a day, plus a monthly restore drill that proves the backups actually work.

**TL;DR:**

```bash
# Install
sudo apt install restic

# Initialize repository (one-time)
export RESTIC_REPOSITORY="s3:https://s3.eu-central-1.amazonaws.com/my-backup-bucket"
export RESTIC_PASSWORD_FILE="/root/.restic-password"
restic init

# Backup (runs nightly via systemd timer)
restic backup /etc /home /var/lib /opt --tag nightly

# Prune old backups (runs weekly)
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune

```

Each backup after the first is fast — only changed file blocks are uploaded. A 200 GB box with 1 GB of daily churn does its nightly in 3-5 minutes.

## Why Restic over alternatives

- **vs rsync:** rsync's a sync, not a backup. No retention semantics, no encryption, no deduplication across snapshots. Identical files in 10 daily snapshots = 10× storage.
- **vs Borg:** Borg is excellent but only writes to filesystems (or via SSH). Restic writes natively to S3, B2, Azure, GCS, MinIO, REST server. Easier offsite without running an SSH-accessible host.
- **vs Veeam / commercial:** they work, they're expensive, and the on-premises ones have their own attack surface. Restic is a single static binary, no agent, no service.
- **vs `tar | gzip`:** see "no retention semantics, no dedup."

The one downside: restoring a single file from a 1 TB repo takes a few seconds longer than `cp` from a directory backup. That's the tradeoff for cryptographic integrity and dedup.

## Pick a backend

**MinIO** (self-hosted S3-compatible): if you already run a homelab box with disk to spare, run MinIO there. €0/month after the hardware. Don't put MinIO on the same box you're backing up — defeats the purpose.

**Hetzner Object Storage / Wasabi / Backblaze B2:** cheap commercial S3-compatible. \~€5-10/month for a few hundred GB. No egress fees on B2 — important for restores.

**AWS S3:** more expensive than B2/Wasabi, but the ecosystem is broadest. Use Glacier Deep Archive for long-term, S3 Standard for active.

I use a mix: hot copy to MinIO (fast restore), cold copy to B2 (offsite, survives the homelab burning down).

## Set up the repository

Pick the repo URL based on backend. For S3:

```bash
export RESTIC_REPOSITORY="s3:https://s3.eu-central-1.amazonaws.com/my-bucket/server01"
export AWS_ACCESS_KEY_ID="AKIA..."
export AWS_SECRET_ACCESS_KEY="..."

```

For self-hosted MinIO:

```bash
export RESTIC_REPOSITORY="s3:http://minio.lab.example.com:9000/backup/server01"
export AWS_ACCESS_KEY_ID="restic-server01"
export AWS_SECRET_ACCESS_KEY="..."

```

Each server gets its own subpath in the bucket (`server01`, `server02`...). Don't share a single repo across multiple sources — restoring one corrupts the other on the rare bad day.

Set the repo password (this encrypts everything):

```bash
echo 'a-long-randomly-generated-password' > /root/.restic-password
chmod 600 /root/.restic-password
export RESTIC_PASSWORD_FILE="/root/.restic-password"

```

**Save this password somewhere outside the server.** Lose it, the encrypted backup is permanently unreadable. Same as a [BitLocker recovery key](/bitlocker-windows-server-os-volume/) — copy it to your password manager and a printout.

Initialize:

```bash
restic init
# created restic repository abc123 at s3:...

```

## First backup

```bash
restic backup /etc /home /var/lib /opt \
  --exclude-file=/root/.restic-excludes \
  --tag nightly

```

Excludes file (`/root/.restic-excludes`):

```
/var/lib/docker/overlay2
/var/lib/lxcfs
/var/cache
/tmp
/proc
/sys
*.pyc
node_modules

```

Don't back up `/var/lib/docker/overlay2` (huge, regenerable), `/var/cache` (regenerable), or anywhere that's a virtual FS. Add app-specific excludes as needed.

First backup uploads everything. A 50 GB box takes 30-60 minutes depending on bandwidth. Subsequent backups are minutes.

## Verify

After the first backup:

```bash
restic snapshots
# ID        Time                 Host        Tags    Paths
# abcdef12  2026-04-27 02:00:00  server01    nightly /etc, /home, /var/lib, /opt

restic stats
# Total File Count:   24532
# Total Size:        18.234 GiB

```

Check repository integrity:

```bash
restic check
# load indexes
# check all packs
# check snapshots, trees and blobs
# no errors were found

```

Run `check --read-data` monthly. Slower (downloads every blob) but catches silent corruption.

## Automate with systemd timer

`/etc/systemd/system/restic-backup.service`:

```ini
[Unit]
Description=Restic backup
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
EnvironmentFile=/root/.restic-env
ExecStart=/usr/bin/restic backup /etc /home /var/lib /opt \
  --exclude-file=/root/.restic-excludes \
  --tag nightly
ExecStartPost=/usr/bin/restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune

```

`/root/.restic-env`:

```
RESTIC_REPOSITORY=s3:https://...
RESTIC_PASSWORD_FILE=/root/.restic-password
AWS_ACCESS_KEY_ID=AKIA...
AWS_SECRET_ACCESS_KEY=...

```

Permissions: `chmod 600 /root/.restic-env`.

`/etc/systemd/system/restic-backup.timer`:

```ini
[Unit]
Description=Run Restic backup daily

[Timer]
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=30m
Persistent=true

[Install]
WantedBy=timers.target

```

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now restic-backup.timer

```

`RandomizedDelaySec` smears load across hosts so 100 servers don't all hit S3 at exactly 02:00\. `Persistent=true` runs missed schedules after reboot.

Same systemd unit pattern as in [systemd service unit files](/systemd-service-unit-file/), just `oneshot` \+ timer instead of `simple`.

## Retention

The `forget --prune` line:

```bash
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune

```

Keeps:

- The last 7 daily snapshots
- The last 4 weekly snapshots
- The last 12 monthly snapshots

23 total snapshots, covering 1 year. Adjust per appetite — `--keep-yearly 5` for compliance retention, `--keep-tag annual --keep-last 3` for labeled long-term.

`--prune` actually removes the data from the backend. Without it, snapshots are forgotten but bytes remain. Always run with `--prune` unless you're auditing what would be deleted.

## The restore drill — actually do this

Backups you've never restored from aren't backups. They're hopes.

Once a month, pick a random file you'd care about losing, restore it to a scratch path, diff against the original:

```bash
restic restore latest --target /tmp/restore-test --include /etc/nginx/nginx.conf
diff /etc/nginx/nginx.conf /tmp/restore-test/etc/nginx/nginx.conf

```

Quarterly: full restore to a separate VM. Boot it, see if applications come up.

Annually: a "what if the source server is gone" drill. Initialize a new VM, install Restic, point at the repo with the same password, restore everything.

I mark these in the calendar. They've caught:

- A wrong-permissions issue where Restic was running as a user that couldn't read `/var/lib/postgresql` — backup looked successful, was empty inside.
- A rotation bug where retention had quietly evicted everything older than 7 days because someone fat-fingered the cron line.
- An MinIO bucket that had quietly hit a quota.

If you don't drill, you don't actually have backups. Drill.

## Cross-server bandwidth

A backup of a 200 GB server pushed to S3 over a 100 Mbit link takes 4-5 hours the first time. Subsequent nightly runs upload only changed blocks — usually 100 MB - 1 GB, finishes in minutes.

If you have many similar servers, the dedup is across hosts when they share a repo path prefix and the same encryption key. I keep separate repos per host (simpler, isolation), so I miss some cross-host dedup. For a homelab with 5 boxes, the storage savings of shared dedup aren't worth the operational risk.

## Gotchas

- **Don't lose the password.** Said it once already. Saying it again. There is no recovery.
- **Don't back up secrets in plaintext.** Restic encrypts the backup, but if someone steals the repo URL + password, they have everything. Treat the password like the data itself.
- **`--prune` is slow on large repos.** A 1 TB repo with months of history takes 30-60 min to prune. Schedule weekly, not nightly, on big sets.
- **MinIO needs versioning OFF for the bucket.** Restic manages its own versions; MinIO/S3 versioning on top doubles or triples storage.
- **Don't back up `/var/lib/mysql` directly while MySQL is running.** Use `mysqldump` (or PITR via binlogs) into a directory, then back up the directory. Same for Postgres / MongoDB. File-level backups of running databases give you Frankenstein corruption.
- **Restic processes are CPU-bound.** Encryption + dedup hashing on a 4-core box will pin one core. Schedule for off-hours; use `nice -n 19 ionice -c3` if you must run during work hours.

## How this combines with other things

- [rsync setup](/rsync-backup-linux/) — fine for fast local snapshots; Restic is the offsite layer above it.
- [linux-disk-full forensics](/linux-disk-full-forensics/) — when prune misbehaves and your backup target fills, that post is the recovery.
- [SSH hardening](/ssh-hardening-ubuntu-checklist/) — apply before storing repo credentials on a server.

The setup above is what I'd recommend to anyone who has more than "a couple of family photos" to back up. It's not the simplest tool, but it's the one I trust at 3am when everything else has gone wrong.

---

### Related posts

- [Netdata on Ubuntu — Live Server Metrics in 5 Minutes](https://blog.diengdoh.com/netdata-monitoring-ubuntu/?ref=blog.sudowl.com)
- [WireGuard VPN on Ubuntu — the Setup I Actually Use](https://blog.diengdoh.com/wireguard-vpn-server-ubuntu/?ref=blog.sudowl.com)
- [SSH Hardening Checklist for Ubuntu Servers](https://blog.diengdoh.com/ssh-hardening-ubuntu-checklist/?ref=blog.sudowl.com)