Bulletproof Off-Site Backup for Your Seedbox and Media Server
If you're running Jellyfin, Nextcloud, Immich, or any Docker-based stack on a seedbox or home server, your backup strategy is either saving you or waiting to fail you. Most people either skip backups entirely or do something half-baked that won't survive a real disaster.
This guide covers a practical, layered approach: Borg for local/remote deduplication + rclone for cloud offsite, with specific notes for ZFS users and Docker volume handling.
What You Actually Need to Back Up
Before touching a single command, get clear on what matters:
| Data Type | Examples | Priority |
|---|---|---|
| Application databases | Nextcloud DB, Immich Postgres | Critical |
| App config/metadata | Jellyfin metadata, Sonarr DB | High |
| User-uploaded content | Immich photos, Nextcloud files | High |
| Docker volumes | All of the above, usually in /var/lib/docker/volumes/ | High |
| Media library | Movies, TV shows, music | Low — rerippable/re-downloadable |
Don't waste backup space on your media library. If you're on a seedbox or private tracker, you can re-download it. Focus your backup budget on the irreplaceable stuff: databases, configs, and personal uploads.
The Stack: Borg + rclone
- BorgBackup — deduplicating, compressed, encrypted backups. Ideal for incremental daily snapshots.
- rclone — syncs Borg repos (or anything) to cloud storage: Backblaze B2, Wasabi, S3, or even another seedbox via SFTP.
Together they give you: local fast restore + off-site disaster recovery.
Step 1: Handle Databases Before You Back Up Anything
Never back up a running database by just copying files. You'll get a corrupted or inconsistent snapshot. Always dump first.
Nextcloud (with AIO)
# Stop AIO stack, run the built-in backup
docker exec -it nextcloud-aio-mastercontainer php occ maintenance:mode --on
# Then proceed with Borg
Or use Nextcloud AIO's built-in backup feature which handles this for you.
Immich (Postgres)
docker exec -t immich_postgres pg_dumpall -c -U postgres > /backup/immich_dump_$(date +%Y%m%d).sql
Jellyfin
Jellyfin doesn't use a traditional DB — its metadata lives in the config directory. Just stop the container before backing up:
docker stop jellyfin
# run borg
docker start jellyfin
Sonarr / Radarr / Prowlarr
These use SQLite. Back up the entire /config directory while the container is stopped, or use the built-in System → Backup feature which creates a safe .zip.
Step 2: Set Up a Borg Repository
Install Borg on your server:
# Debian/Ubuntu
sudo apt install borgbackup
# Or latest via pip
pip install borgbackup
Initialize a repo (with encryption):
borg init --encryption=repokey /mnt/backup/borg-repo
Save your passphrase somewhere safe — without it, your backups are unrecoverable.
Step 3: The Backup Script
Here's a practical script that stops containers, dumps databases, runs Borg, then restarts everything:
#!/bin/bash
set -e
BORG_REPO="/mnt/backup/borg-repo"
export BORG_PASSPHRASE="your-passphrase-here"
BACKUP_DIRS="/opt/docker/configs /backup/db-dumps"
LOG="/var/log/borg-backup.log"
echo "=== Backup started: $(date) ===" >> $LOG
# 1. Dump databases
docker exec immich_postgres pg_dumpall -c -U postgres > /backup/db-dumps/immich_$(date +%Y%m%d).sql
# 2. Stop containers that need consistent state
docker stop jellyfin sonarr radarr prowlarr
# 3. Run Borg
borg create \
--verbose \
--filter AME \
--list \
--stats \
--show-rc \
--compression lz4 \
--exclude-caches \
::'{hostname}-{now}' \
$BACKUP_DIRS \
2>> $LOG
# 4. Prune old backups (keep 7 daily, 4 weekly, 3 monthly)
borg prune \
--list \
--prefix '{hostname}-' \
--keep-daily 7 \
--keep-weekly 4 \
--keep-monthly 3 \
2>> $LOG
# 5. Restart containers
docker start jellyfin sonarr radarr prowlarr
echo "=== Backup finished: $(date) ===" >> $LOG
Schedule it with cron:
# Run at 3am daily
0 3 * * * /opt/scripts/borg-backup.sh
Step 4: Off-Site Sync with rclone
A local Borg repo is not a backup — it's on the same machine that can fail. Push it off-site.
Configure rclone with your preferred backend (Backblaze B2 is cheap at ~$6/TB/month):
rclone config
# Follow prompts to add b2, s3, wasabi, or sftp
Sync your Borg repo after each backup:
rclone sync /mnt/backup/borg-repo remote:my-bucket/borg-repo \
--transfers 4 \
--checkers 8 \
--fast-list \
--log-file /var/log/rclone-backup.log
Add this to the end of your backup script. Total off-site cost for configs + DB dumps (typically under 50GB) is under $0.30/month on B2.
ZFS Users: Snapshots Are Not Backups
If you're running a ZFS pool, you have snapshots — but snapshots on the same pool don't protect against drive failure, accidental deletion propagating, or pool corruption. Use them as a fast local rollback layer, not a substitute for Borg + rclone.
Useful ZFS snapshot automation: sanoid
# Install sanoid
sudo apt install sanoid
# Configure /etc/sanoid/sanoid.conf
[data/docker]
use_template = production
[template_production]
frequently = 0
hourly = 24
daily = 30
monthly = 3
autosnap = yes
autoprune = yes
ZFS snapshots + Borg + rclone = a genuinely resilient setup.
Testing Your Restore (Do This Now)
A backup you've never tested is a backup you don't have. Run this quarterly:
# List available archives
borg list /mnt/backup/borg-repo
# Extract a specific archive to a test location
borg extract --dry-run /mnt/backup/borg-repo::your-archive-name
# Actually extract to /tmp/restore-test
borg extract /mnt/backup/borg-repo::your-archive-name --target /tmp/restore-test
For databases, restore the dump to a test container and verify the data looks right.
Practical Takeaway
The minimum viable backup for a Jellyfin/Nextcloud/Immich stack:
- Daily DB dumps to a local directory
- Borg backup of configs + dumps with 7-day retention
- rclone sync of the Borg repo to Backblaze B2 or Wasabi
- Monthly restore test — actually extract something and check it
Total cost: ~$0–5/month depending on data size. Time to set up: 2–3 hours. Time you'll save when something breaks: priceless.
Don't be the person who discovers their backup was broken the day they need it.