Backing Up Self-Hosted Docker Services with Restic
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.