Backing Up to Object Storage - restic with the MinIO S3 Backend
Status: Active
Last Updated: 2026-08-26
Category: Cloud - Backup Implementation
Prerequisites: restic-backups, minio-setup, s3-api-usage
Time: 2 hours
Tags: restic, backup, s3, minio, prune, restore
Summary
End-to-end configuration of restic with an S3 backend on MinIO: repository initialization, snapshot workflows including stdin database dumps, retention and prune scheduling, integrity checking with restic check, and โ most importantly โ rehearsed restore procedures. This is Tier 2 of the strategy defined in storage-backup-strategies.
๐ฏ What You'll Learn
By the end of this article, you'll be able to:
- โ Initialize and configure a restic repository on S3/MinIO
- โ Back up directories and live database dumps via stdin
- โ
Run retention (
forget) and garbage collection (prune) safely on schedules - โ Verify repository health with scheduled checks
- โ Perform and rehearse full and single-file restores
Table of Contents
- Architecture
- Repository Setup
- Snapshot Workflows
- Retention: forget & prune
- Integrity Checks
- Restore Procedures
Context / Why This Matters
restic-backups covers restic fundamentals with a local-disk repository; here we move the repo to object storage so backups survive loss of the primary host entirely. Restic encrypts client-side (AES-256), so MinIO stores only ciphertext โ the object store does not need to be trusted. Combined with bucket versioning (s3-api-usage), even a compromised backup agent cannot silently destroy history.
Implementation / Core Content
Architecture
Docker hosts / NAS MinIO server Offsite
โโโโโโโโโโโโโโโโโโโโโโโโโ S3 API โโโโโโโโโโโโโโโโ rclone/restic copy
โ pg_dump / mysqldump โโโโผโโโโโโโโโโโถ โ restic โ โโโโโโโโโโโโโโโโโโโโถ
โ /srv/<service>/data โโโโค โ repositories โ encrypted repo
โโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโ
One repository per major data class (or per host) keeps blast radius small: losing one repo's password never costs you everything.
Repository Setup
Prepare the bucket first (see Example 1 in s3-api-usage): dedicated user, versioning enabled.
# Environment used by every restic invocation below.
# Put these in /etc/restic-env (chmod 600, root-owned):
export RESTIC_REPOSITORY="s3:http://minio.internal:9000/restic-nextcloud"
export RESTIC_PASSWORD_FILE="/etc/restic-nextcloud.pass"
export AWS_ACCESS_KEY_ID="nc-backup"
export AWS_SECRET_ACCESS_KEY="<scoped-password>"
Initialize:
source /etc/restic-env
restic init
# created restic repository <id> at ...
Verify connectivity and that the repo is empty-but-valid:
restic snapshots # "no snapshots" = success
restic stats --mode raw-data
Notes:
- Use plain HTTP only on trusted internal networks; otherwise front MinIO with TLS per ../security/tls-configuration.md.
- The password file is the single point of failure for the whole repo. Keep a second copy offline โ a repo whose password exists only on the same disk as the repo is not a backup.
- restic caches index data under
~/.cache/restic; on servers give it a stable location:export RESTIC_CACHE_DIR=/var/cache/restic.
Snapshot Workflows
Directory backup with tags:
restic backup /srv/nextcloud/data \
--tag nextcloud --tag daily \
--exclude-caches \
--exclude '/srv/nextcloud/data/files_trashbin/**'
Live database via stdin โ avoids torn-file risk of copying running DB directories:
docker exec nextcloud-db pg_dump -U nextcloud nextcloud \
| gzip \
| restic backup --stdin --stdin-filename nextcloud-db.sql.gz \
--tag nextcloud-db --tag daily
MySQL/MariaDB equivalent: mysqldump --single-transaction db | restic backup --stdin ....
Consistency ordering matters: dump the database first, then back up files, so both halves of the snapshot pair correspond to approximately the same moment. For larger setups, freeze/thaw scripts or LVM/ZFS snapshots bracket the file backup instead.
List and compare snapshots:
restic snapshots --latest 5
restic diff <snap-id-a> <snap-id-b>
Retention: forget & prune
Two distinct operations, commonly confused:
forgetremoves snapshots from the index per policy (data still occupies space)prunegarbage-collects unreferenced data blobs (the actual space reclaim)
# Policy: keep 7 daily, 5 weekly, 12 monthly, 3 yearly; never delete yearly-tagged
restic forget \
--keep-daily 7 --keep-weekly 5 --keep-monthly 12 --keep-yearly 3 \
--tag daily \
--group-by paths,tags \
--dry-run # ALWAYS preview first; drop flag when satisfied
restic prune # after forget; reclaims space
Safer modern variant combining both with safety limits:
restic forget --prune \
--keep-daily 7 --keep-weekly 5 --keep-monthly 12 \
--max-repack-size 20G \ # cap per-run network/CPU churn
--verbose
Schedule: nightly backup, then forget --prune weekly (Sunday). Daily prune of large S3 repos wastes bandwidth repacking pack files for little gain.
Integrity Checks
Backups rot silently unless checked. Two levels:
restic check --read-data-subset=10% # weekly: metadata + 10% of data packs
restic check --read-data # quarterly: full read of every blob
check without flags validates structure/index only (fast, run after every prune). If it reports damaged packs, repair by re-uploading from a good source or restore what's readable and rebuild โ never ignore errors.
Restore Procedures
Single file or directory browse + extract:
restic snapshots # pick snapshot ID
restic ls latest --tag nextcloud # list contents
restic restore latest:/srv/nextcloud/data/photos/beach.jpg \
--target /tmp/restore-inplace # recreates full path under target
restic mount /mnt/restic-archive # or FUSE-mount whole history
ls /mnt/restic-archive/snapshots/<id>/...
Full bare-metal-style restore to a fresh machine:
source /etc/restic-env
restic restore latest --target /
# then re-deploy services from compose files (git-tracked config),
# restore DB dumps:
gunzip -c nextcloud-db.sql.gz | docker exec -i nextcloud-db \
psql -U nextcloud nextcloud
Restore speed over LAN to MinIO is typically disk-bound, not network-bound, thanks to deduplicated pack reads โ but first-time restores of huge trees benefit from --include scoping: restore the OS/service layer first, bulk data second.
Practical Examples
Example 1: Complete new-service backup recipe
# 1. Bucket + scoped user (see s3-api-usage)
mc mb local/restic-immich && mc version enable local/restic-immich
# 2. Env file
cat > /etc/restic-immich.env <<'EOF'
RESTIC_REPOSITORY=s3:http://minio.internal:9000/restic-immich
RESTIC_PASSWORD_FILE=/etc/restic-immich.pass
AWS_ACCESS_KEY_ID=immich-backup
AWS_SECRET_ACCESS_KEY=<password>
EOF
# 3. Init + first run
source /etc/restic-immich.env && restic init
/opt/restic-scripts/immich-backup.sh && restic snapshots
Example 2: Weekly maintenance script
#!/usr/bin/env bash
set -euo pipefail
source /etc/restic-env
restic forget --prune \
--keep-daily 7 --keep-weekly 5 --keep-monthly 12 \
--max-repack-size 20G
restic check --read-data-subset=10%
echo "OK $(date -Is)" >> /var/log/restic-maintenance.log
Exit code non-zero on any failure โ wire it into alerting (backup-automation).
Example 3: Quarterly restore drill
Pick one service at random each quarter; time the full restore and record RTO achieved vs target from storage-backup-strategies:
time restic restore latest --target /tmp/drill-immich
diff -r /tmp/drill-immich/srv/immich/library /srv/immich/library | head
A drill that was never timed has no measured RTO โ log results in the ops journal.
Troubleshooting & Common Pitfalls
| Problem | Cause | Fix |
|---|---|---|
Fatal: wrong password on healthy repo |
Wrong env file loaded; multiple repos | Always source explicit env; label shells |
| Prune takes hours / heavy bandwidth | Daily prune repacking large packs | Prune weekly; set --max-repack-size; use --repack-cacheable-only |
check reports damaged packs |
Interrupted upload, bitrot, disk fault on MinIO | Restore readable data, re-init repo; check MinIO drive health |
| Backup succeeds but restore fails | Never tested; password lost; repo corrupt | Quarterly drills; offline password copies |
S3 AccessDenied mid-backup |
Scoped user missing DeleteObject for prune | Extend policy actions; see s3-api-usage |
| Slow first backup to S3 | Small files dominate; cache disabled | Set RESTIC_CACHE_DIR to persistent local disk |
| Repo grows unbounded despite forget | Forget policy not matching tags used in backup | Align --group-by and tag filters between commands |
Next Steps / Ops Actions
- Wire backup/prune/check scripts into systemd timers with notifications: backup-automation
- Add offsite replication as Tier 3: storage-backup-strategies
- Calendarize the quarterly full-read
restic check --read-dataand restore drills - Confirm MinIO bucket versioning + quota are set on all restic buckets
Sources & Related Articles
External references consulted:
- https://restic.readthedocs.io/en/stable/040_backup.html
- https://restic.readthedocs.io/en/stable/060_forget.html
- https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html#amazon-s3
Related knowledge-base articles:
Change Log
2026-08-26
- Initial creation by KB writing session.