Backing Up Self-Hosted Docker Services with Restic#

Self-hosting is great until you lose everything. A dead SD card, a botched upgrade, or a misconfigured volume mount can wipe out months of data in seconds. If you’re running services like Nextcloud, Immich, or Linkding on a Raspberry Pi or home server, you need a backup strategy that actually works.

I use Restic with a Hetzner Storage Box. Here’s how I set it up.

Why Restic?#

There are plenty of backup tools out there. Restic stands out for a few reasons:

  • Deduplication — only changed data gets uploaded after the first backup
  • Encryption — data is encrypted before it leaves your machine
  • Multiple backends — works with local drives, SFTP, S3, Backblaze B2, and more
  • Fast restores — browse and restore individual files without downloading everything

It’s also simple. No daemons, no complex configuration files. Just a single binary and a few commands.

Why Hetzner Storage Box?#

I wanted offsite backups but didn’t want to rely on big cloud providers. Hetzner Storage Box is cheap (€3.81/month for 1TB), based in Europe, and supports SFTP out of the box. Since Restic encrypts everything client-side, Hetzner only ever sees encrypted blobs.

There’s a certain irony in escaping Big Tech only to store backups on someone else’s server. But the encryption means I’m not trusting them with my data — just with storing opaque files. If Hetzner disappeared tomorrow, I’d lose access to my offsite backups but not my privacy.

My Setup#

I run Docker services on a Raspberry Pi 4. Each application lives in its own directory:

/home/bist/
├── linkding/
│   ├── docker-compose.yml
│   └── data/
├── immich/
│   ├── docker-compose.yml
│   └── data/
└── nextcloud/
    ├── docker-compose.yml
    └── data/

Config and data together, per application. This makes backups straightforward — grab the whole folder and you have everything needed to restore that service.

Setting Up Restic with Hetzner#

Install Restic#

sudo apt install restic

Configure SSH Access#

Hetzner Storage Box uses SFTP on port 23. First, upload your SSH key:

echo "mkdir .ssh" | sftp -P 23 uXXXXXX@uXXXXXX.your-storagebox.de
scp -P 23 ~/.ssh/id_rsa.pub uXXXXXX@uXXXXXX.your-storagebox.de:.ssh/authorized_keys

Test the connection:

sftp -P 23 uXXXXXX@uXXXXXX.your-storagebox.de

If it connects without a password prompt, you’re good.

Initialize the Repository#

Create a directory for your backups and initialize Restic:

echo "mkdir backups" | sftp -P 23 uXXXXXX@uXXXXXX.your-storagebox.de

export RESTIC_REPOSITORY="sftp:uXXXXXX@uXXXXXX.your-storagebox.de:23:/backups"
export RESTIC_PASSWORD="your-strong-encryption-password"

restic init

Pick a strong password. Write it down somewhere safe — a password manager, a printed copy in a drawer, wherever. If you lose this password, your backups are gone. Restic has no recovery mechanism.

Create an Environment File#

Rather than exporting variables every time, create a file:

~/.restic-env:

export RESTIC_REPOSITORY="sftp:uXXXXXX@uXXXXXX.your-storagebox.de:23:/backups"
export RESTIC_PASSWORD="your-strong-encryption-password"

Lock it down:

chmod 600 ~/.restic-env

Now you can just run source ~/.restic-env before any Restic command.

The Backup Script#

I wrote a simple script that backs up each application directory:

#!/bin/bash

set -e

export RESTIC_REPOSITORY="sftp:uXXXXXX@uXXXXXX.your-storagebox.de:23:/backups"
export RESTIC_PASSWORD="your-strong-encryption-password"

BACKUP_DIRS=(
    "/home/bist/linkding"
    "/home/bist/immich"
    "/home/bist/nextcloud"
)

LOG_FILE="/home/bist/logs/restic-backup.log"
mkdir -p "$(dirname "$LOG_FILE")"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

log "=== Starting backup ==="

for dir in "${BACKUP_DIRS[@]}"; do
    if [[ -d "$dir" ]]; then
        log "Backing up: $dir"
        restic backup "$dir" --exclude="*.log" --exclude="*.tmp"
    else
        log "Warning: $dir does not exist, skipping"
    fi
done

log "Pruning old snapshots..."
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

log "=== Backup complete ==="

restic snapshots --latest 3

Adding a new service means adding one line to the BACKUP_DIRS array.

The retention policy keeps the last 7 daily backups, 4 weekly, and 6 monthly. Old snapshots get pruned automatically, so storage doesn’t grow forever.

Automating with Cron#

I run backups nightly at 3 AM:

crontab -e
0 3 * * * /home/bist/scripts/backup.sh

Logs go to /home/bist/logs/restic-backup.log so I can check what happened if something seems off.

Restoring#

Backups are worthless if you can’t restore them. Here’s how.

List Snapshots#

source ~/.restic-env
restic snapshots

This shows all your backups with timestamps and IDs.

Restore an Application#

Always restore to a temporary location first:

restic restore latest --target /tmp/restore --include "/home/bist/linkding"

Check it looks right, then swap it into place:

cd /home/bist/linkding && docker compose down
mv /home/bist/linkding /home/bist/linkding.broken
mv /tmp/restore/home/bist/linkding /home/bist/
docker compose up -d

Once you’ve verified everything works, delete the broken copy.

Restore a Single File#

restic restore latest --target /tmp/restore --include "/home/bist/linkding/docker-compose.yml"

Browse Backups Interactively#

Restic can mount snapshots as a filesystem:

mkdir -p /tmp/restic-mount
restic mount /tmp/restic-mount

Browse with your file manager or terminal, copy what you need, then unmount.

The Most Important Step#

Test your restores. A backup you’ve never tested is just a hope. Once a month or so, I pick a random service and restore it to /tmp just to make sure everything actually works.

What About Databases?#

SQLite-based services (like Linkding) are fine — the database file gets backed up with everything else.

For services with MySQL or PostgreSQL, you should dump the database before the backup runs. For example, with Mailcow:

./helper-scripts/backup_and_restore.sh backup all

Then include the dump directory in your backup paths.

The 3-2-1 Rule#

This setup gives me:

  • 3 copies: Live data + local snapshots + Hetzner
  • 2 media types: SD card + cloud storage
  • 1 offsite: Hetzner Storage Box

If my Pi dies, I buy a new one and restore from Hetzner. If Hetzner dies, I still have local data. If my house burns down… well, at least my bookmarks survive.


This post is part of a series on self-hosting and moving away from Big Tech services.