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:


Table of Contents

  1. Architecture
  2. Repository Setup
  3. Snapshot Workflows
  4. Retention: forget & prune
  5. Integrity Checks
  6. 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:

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:

# 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

Sources & Related Articles

External references consulted:

Related knowledge-base articles:

Change Log

2026-08-26

Choose Theme

Your selection is saved locally.

Neural Cacophony
Aperture v2
Flux v1
Mosaic Chaos
Nexus v1
Nexus Zest
Prism v2
Synapse