Self-HostingDocker

Restic Backup for Homelabs: The Setup I Use for Linux Servers, Docker Volumes, and Offsite Copies

Learn the restic backup workflow I use for Linux servers and Docker volumes, with local repositories, retention policies, restore tests, and Backblaze B2 offsite copies.

AU

Author

Marcus Chen

Restic Backup for Homelabs: The Setup I Use for Linux Servers, Docker Volumes, and Offsite Copies

This article contains affiliate links. If you purchase through these links, we may earn a commission at no additional cost to you.

Key Takeaways

  • Restic is one of the few backup tools I trust in a homelab because it is encrypted, deduplicated, scriptable, and boring in the best possible way.
  • A useful backup plan needs three things: a local target, an offsite copy, and a restore test you will actually run.
  • Docker backups fail when you back up inconsistent application data. Stop or snapshot the right workloads first, then run restic.
  • Retention policy matters as much as the backup command. forget and prune are not optional housekeeping.
  • If your backup job does not page you, email you, or at least log somewhere obvious, it will eventually fail in silence (usually on a holiday, because infrastructure enjoys irony).

After running backup jobs across Linux servers, Docker hosts, and NAS boxes for years, I have learned one uncomfortable truth: most homelab backups are a confidence ritual. They create logs, consume storage, and make you feel responsible. Then the first real restore turns into archaeology.

Restic is the tool I keep coming back to because it stays out of the way. It is a single binary, it encrypts data before it leaves your box, and it does not require building your life around a giant backup server. More importantly, it scales from “I have one mini PC and a USB SSD” to “I have three Linux hosts, a NAS, and an offsite bucket because I finally learned from my own mistakes.”

In this guide, I will show you the restic setup I actually use for homelab-style backups. That means Linux servers, Docker volumes, local repositories, offsite copies, sane retention, and restore checks. Not just the happy path either. The parts that usually bite people are the exact parts we are going to cover.

If you want broader planning before you pick a tool, read Homelab Backups and Monitoring: The Boring Setup That Saves You. If you are mostly trying to protect containers, my Docker Backup Strategies guide is the higher-level version of the same problem.

Why I Use Restic Instead of rsync Alone

I still use rsync. It is excellent for fast local copies and migrations. But I do not use rsync alone as my primary backup system anymore.

Restic gives me snapshot history, deduplication, encryption, repository checks, and backend flexibility without much ceremony. I can point the same workflow at a USB SSD, an NFS mount, SFTP storage, or Backblaze B2. That matters in a homelab because the storage target tends to change as your setup grows and your budget changes shape.

The mistake I made: I used to keep “backups” as plain mirrored directories on the same NAS. It worked right up until I needed versioning and realized my corruption had already been mirrored too. Very efficient failure, to be fair.

The Backup Architecture I Recommend

My preferred layout is simple:

  1. Back up each Linux host to a local or LAN-accessible restic repository.
  2. Keep the repository encrypted with a password file.
  3. Run scheduled backups daily.
  4. Apply retention with forget and prune.
  5. Copy critical datasets offsite to Backblaze B2 or another supported backend.
  6. Test restores on a schedule, not only when panic becomes the project manager.

For small labs, a USB SSD attached to the backup host is fine as the first target. For multi-host labs, I like a NAS share or a dedicated backup box. For offsite protection, Backblaze B2 is practical and cheap enough that you do not need a board meeting to justify it.

If you are backing up VMs as well, pair this with Proxmox Backup Strategies: How to Never Lose a VM Again. Restic is excellent for files and application data, but it should complement hypervisor-level backups, not replace them.

Hardware I Would Actually Use

You do not need to buy your way into a backup strategy, but a few pieces of gear make life easier:

Those are not mandatory. They are simply the kind of products that fit this workflow without creating extra drama.

Step 1: Install Restic

On Debian or Ubuntu:

sudo apt update
sudo apt install -y restic

On Fedora:

sudo dnf install -y restic

Confirm the version:

restic version

I also create a dedicated configuration directory:

sudo mkdir -p /etc/restic
sudo chmod 700 /etc/restic

Step 2: Create a Password File and Repository Variables

Do not hardcode the repository password directly into every command. That works, but only in the same way juggling knives technically works.

Create a password file:

sudo sh -c 'openssl rand -base64 32 > /etc/restic/password'
sudo chmod 600 /etc/restic/password

Now create an environment file for a local repository on a mounted backup volume:

sudo tee /etc/restic/restic.env >/dev/null <<'EOF'
export RESTIC_REPOSITORY=/srv/backup/restic
export RESTIC_PASSWORD_FILE=/etc/restic/password
export RESTIC_CACHE_DIR=/var/cache/restic
EOF

Lock it down:

sudo chmod 600 /etc/restic/restic.env
sudo mkdir -p /var/cache/restic /srv/backup/restic

Load it and initialize the repository:

source /etc/restic/restic.env
restic init

If you prefer a NAS target, mount it first. My comparison of NFS vs SMB vs iSCSI explains why I usually pick NFS for Linux-heavy labs.

Step 3: Define What You Will Back Up

This is the part people skip because “everything important is under /opt.” That sentence has ruined many evenings.

For a typical Linux + Docker host, I back up:

  • /etc
  • /home
  • /opt
  • Docker Compose directories
  • bind-mounted application data
  • database dumps if the app stores live state in a database
  • custom scripts, systemd unit files, and reverse proxy configs

Create include and exclude files:

sudo tee /etc/restic/include.txt >/dev/null <<'EOF'
/etc
/home
/opt
/var/lib/docker/volumes
/usr/local/bin
EOF

sudo tee /etc/restic/exclude.txt >/dev/null <<'EOF'
/var/lib/docker/overlay2
/tmp
/var/tmp
/var/cache
/home/*/.cache
EOF

I generally avoid backing up Docker's overlay2 filesystem directly. It is large, noisy, and often not what you actually need. The persistent data usually lives in bind mounts or named volumes. Back up the data you can restore with confidence, not the storage internals you hope will somehow make sense later.

Step 4: Handle Docker Data Properly

Here is the thing people get wrong with container backups: a running container is not the problem. Inconsistent writes are the problem.

Static services are easy. Databases are not. If you back up a busy PostgreSQL or MariaDB volume without coordination, you may get a snapshot that exists but is not trustworthy. That is a terrible category of success.

For low-write containers, I often stop the container briefly before backup:

docker compose -f /opt/immich/docker-compose.yml stop immich_postgres
restic backup /opt/immich --tag docker --tag immich

docker compose -f /opt/immich/docker-compose.yml start immich_postgres

For databases, I prefer an application-aware dump plus the volume backup. Example for PostgreSQL:

mkdir -p /opt/backups/postgres

docker exec -t postgres \
  pg_dumpall -U postgres \
  > /opt/backups/postgres/postgres-$(date +%F).sql

restic backup /opt/backups/postgres /srv/apps --tag docker --tag postgres

For MariaDB:

mkdir -p /opt/backups/mariadb

docker exec mariadb \
  mariadb-dump --all-databases -uroot -p'yourpassword' \
  > /opt/backups/mariadb/mariadb-$(date +%F).sql

restic backup /opt/backups/mariadb /srv/apps --tag docker --tag mariadb

Yes, that means a little more scripting. It also means your restores are grounded in reality instead of optimism.

Step 5: Create the Backup Script

This is a trimmed version of the pattern I actually trust:

sudo tee /usr/local/bin/restic-backup.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

source /etc/restic/restic.env
LOG_FILE=/var/log/restic-backup.log
HOST_TAG=$(hostname -s)

exec >> "$LOG_FILE" 2>&1

echo "=== $(date -Is) backup start ==="

# Example: dump PostgreSQL before backup
if docker ps --format '{{.Names}}' | grep -q '^postgres$'; then
  mkdir -p /opt/backups/postgres
  docker exec -t postgres pg_dumpall -U postgres > /opt/backups/postgres/postgres-$(date +%F).sql
fi

restic backup \
  --files-from /etc/restic/include.txt \
  --exclude-file /etc/restic/exclude.txt \
  --tag "$HOST_TAG" \
  --tag daily

restic forget \
  --tag "$HOST_TAG" \
  --group-by host,tags \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 6 \
  --keep-yearly 1

restic prune
restic check --read-data-subset=5%

echo "=== $(date -Is) backup end ==="
EOF

sudo chmod 750 /usr/local/bin/restic-backup.sh

A few notes:

  • I use --group-by host,tags because shared repositories become messy if you do not scope retention properly.
  • I run check with a subset on the daily job. Full --read-data checks are useful, but not every night unless your repo is tiny and your patience is infinite.
  • Logging to a file is the minimum. Realistically, ship the result to email, Discord, or your monitoring stack.

If you already run Grafana and Prometheus, pair this with Homelab Monitoring with Prometheus and Grafana so failed jobs stop being a surprise.

Step 6: Schedule It with systemd, Not Cron

Cron works. I still use it sometimes. But systemd timers are easier to inspect and recover from on modern Linux hosts.

Create the service unit:

# /etc/systemd/system/restic-backup.service
[Unit]
Description=Restic backup job
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/restic-backup.sh
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=7

Create the timer:

# /etc/systemd/system/restic-backup.timer
[Unit]
Description=Run restic backup nightly

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

[Install]
WantedBy=timers.target

Enable it:

sudo systemctl daemon-reload
sudo systemctl enable --now restic-backup.timer
systemctl list-timers restic-backup.timer

Persistent=true is important. If the server is off during the scheduled time, the job runs on next boot instead of pretending nothing happened. Which is nice. I appreciate honesty in infrastructure.

Step 7: Add an Offsite Copy with Backblaze B2

A local backup without an offsite copy is better than nothing. It is not enough if theft, fire, filesystem corruption, or a dramatic power event takes out your primary data and your backup target together.

For Backblaze B2, create a bucket and application key first. Their official guide is here: How to Use Restic for Backups with Backblaze B2 Cloud Storage.

Then create a second environment file:

sudo tee /etc/restic/restic-b2.env >/dev/null <<'EOF'
export RESTIC_REPOSITORY=b2:my-homelab-restic:server01
export RESTIC_PASSWORD_FILE=/etc/restic/password
export B2_ACCOUNT_ID=your-key-id
export B2_ACCOUNT_KEY=your-application-key
EOF

sudo chmod 600 /etc/restic/restic-b2.env

Initialize the offsite repo:

source /etc/restic/restic-b2.env
restic init

Run a backup:

restic backup /etc /home /opt/backups/postgres --tag offsite --tag $(hostname -s)

You can keep the local and offsite repositories separate, which is what I prefer. It keeps restore logic cleaner and lets you apply different retention policies depending on cost and bandwidth.

How I Pick Retention Without Hoarding Everything Forever

Retention is where backup plans quietly become storage problems. The beginner mistake is keeping far too little. The more advanced mistake - which feels very responsible at first - is keeping everything forever until the repository turns into a financial and operational personality test.

My default homelab policy is:

  • 7 daily snapshots
  • 4 weekly snapshots
  • 6 monthly snapshots
  • 1 yearly snapshot

That is enough history to recover from the usual nonsense: a broken update, accidental deletion, silent config damage, or a database mistake you notice next week instead of tonight.

If I have a high-churn dataset, I usually split it into its own repository or at least its own tag. That way a noisy media workflow or camera ingest box does not force the same retention policy onto a low-change config backup.

Always dry-run the policy first:

restic forget \
  --tag $(hostname -s) \
  --group-by host,tags \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 6 \
  --keep-yearly 1 \
  --dry-run

If you share a repository across hosts, not scoping retention by host or tag is how you accidentally become your own outage.

Step 8: Test Restores Before You Need Them

This is where the real backup strategy starts. Everything before this is preparation.

List snapshots:

source /etc/restic/restic.env
restic snapshots

Restore a single directory to a scratch location:

sudo mkdir -p /restore-test
sudo restic restore latest --target /restore-test

Restore a single file:

restic restore latest --target /restore-test --include /etc/ssh/sshd_config

Browse snapshot contents:

restic ls latest

My rule is simple: every critical service gets a restore drill. For a Docker app, that means I restore its Compose file, data directory, and any database dump into a test path and verify that the structure is actually usable.

The mistake I made: for too long, I treated “restic backup completed successfully” as proof. It only proves a command exited cleanly. A restore test proves you are not lying to yourself.

Common Mistakes I See Repeatedly

1. Backing up live databases without a dump or snapshot plan

This is the classic one. The backup exists. The restore is cursed.

2. Keeping the repository password only on the host being protected

If the host dies and the password file dies with it, you have created encrypted decorative storage. Keep a copy in a password manager and a secure offline note.

3. Ignoring retention until the repository gets huge

Restic will not clean up after you unless you tell it to. Read the official docs for backups and forget/prune policies. They are worth your time.

4. Treating Docker internals as the backup target

Back up the application data, not every transient layer in the engine.

5. Running backups with no alerting

At minimum, check systemctl status restic-backup.service and journalctl -u restic-backup.service. Better yet, wire failures into your normal monitoring path.

What to Learn Next

Once this is running reliably, I would expand in this order:

  1. Add backup metrics or alerting.
  2. Separate high-churn and low-churn datasets into different retention policies.
  3. Add hypervisor-aware backups for VMs and NAS-native snapshots where appropriate.
  4. Run a full restore drill quarterly.

That is how you get from “I have backups” to “I can survive a bad day without rebuilding my life from screenshots.”

FAQ

Is restic good for Docker backups?

Yes, if you back up the right data. Restic is excellent for bind mounts, named volumes, and exported database dumps. It is not magic - you still need consistency for stateful apps.

Should I use one repository for every host?

For most homelabs, separate repositories per host or per major role are easier to manage. Shared repositories can work, but you need careful tagging and retention scoping.

Is Backblaze B2 required?

No. Any backend restic supports can work. I like B2 because it is simple, affordable, and common in homelab circles, but SFTP, local disks, and NAS targets are all viable.

How often should I run restic check?

I like a lightweight subset check on daily jobs and a fuller integrity check on a slower schedule, such as weekly or monthly, depending on repository size.

Can I restore just one file instead of the whole backup?

Yes. That is one of restic's best features. You can list snapshots, inspect contents, and restore a single file or directory without pulling back the entire dataset.

Final Recommendation

If you want one backup tool that works across Linux servers, Docker hosts, and offsite object storage without demanding an enterprise-sized attention span, restic is the one I recommend most often.

Set it up once. Script it properly. Test restores on purpose. Then go do something more interesting with your homelab than wondering whether your backups are imaginary.