Transport via Rsync to a Hetzner Storage Box
Wire transfer.sh so every successful backup is immediately rsync'd to a Hetzner Storage Box. This pattern uses one cron job (the backup, which calls the transport hook at the end) and zero external agents.
The setup pattern is the same for any rsync-reachable target. Replace the storage box hostname with your own server when adapting.
Prerequisites
- An active Hetzner Storage Box. The same applies to any rsync-over-SSH target.
- The Storage Box username and base hostname (e.g.
u123456@u123456.your-storagebox.de). - Backup setup already enabled on the Operaide host. See Enable Backups first.
rsyncinstalled on the host (most distributions ship it;apt-get install rsyncif missing).
Step 1: generate an SSH key for unattended access
On the Operaide host, generate a dedicated keypair for the backup transfer. Cron jobs cannot type passphrases, so the key has none.
sudo -u root ssh-keygen -t ed25519 -f /root/.ssh/operaide_backup -N "" -C "operaide-backup@$(hostname)"
The private key lands at /root/.ssh/operaide_backup, the public key at /root/.ssh/operaide_backup.pub. The cron job runs as root by default, so root's home is the right place.
Step 2: install the public key on the Storage Box
Hetzner Storage Boxes accept SSH keys via SFTP into a fixed .ssh/authorized_keys path. From the Operaide host:
cat /root/.ssh/operaide_backup.pub | ssh -p 23 u123456@u123456.your-storagebox.de install-ssh-key
Hetzner provides the install-ssh-key helper. The first connection prompts for the Storage Box password; subsequent rsync runs use the new key.
Verify the key works:
ssh -i /root/.ssh/operaide_backup -p 23 u123456@u123456.your-storagebox.de \
"mkdir -p ./operaide && ls -la ./"
This should connect without prompting and list the root of your Storage Box.
Step 3: edit transfer.sh
Open <project>/backup/transfer.sh and replace the placeholder body with the rsync invocation. The script receives the absolute path of the freshly written full-backup tar as $1.
#!/usr/bin/env bash
# transfer.sh — rsync to Hetzner Storage Box.
#
# Called by operaide-outer-backup.sh with $1 = path to the new tar.
# `--remove-source-files` deletes the local tar after a successful copy
# so the backup volume does not fill up. The deletion is the
# "transferred" signal the backup-status UI watches for.
set -euo pipefail
REMOTE="u123456@u123456.your-storagebox.de"
REMOTE_DIR="./operaide"
SSH_KEY="/root/.ssh/operaide_backup"
rsync -av --remove-source-files \
-e "ssh -i $SSH_KEY -p 23 -o StrictHostKeyChecking=accept-new" \
"$1" "$1.sha256" \
"$REMOTE:$REMOTE_DIR/"
Replace the REMOTE, REMOTE_DIR, and SSH_KEY values to match your account.
--remove-source-files is the recommended option: it removes the local tar after the upload succeeds, which counts as "transferred" in the backup-status UI and frees disk space on the Operaide host. If you want to keep a local copy, drop the flag and replace it with a touch "$1.transferred" after a successful run.
Make the file executable and verify the syntax:
chmod +x <project>/backup/transfer.sh
bash -n <project>/backup/transfer.sh
Step 4: dry-run the transport against an existing tar
Before relying on the cron job, run the transfer once by hand against the most recent backup tar:
cd <project>
bash backup/transfer.sh backup/backups/full-backup-YYYYMMDD-HHMMSS.tar
Expected: rsync logs the upload, the local tar disappears, and the file shows up under ./operaide/ on the Storage Box.
Verify on the remote side:
ssh -i /root/.ssh/operaide_backup -p 23 u123456@u123456.your-storagebox.de \
"ls -la ./operaide/"
Step 5: confirm the transferred badge
Refresh the System Admin -> Backups page. The Outer-block card's title should show Transferred (green check). If the tar is still there and you see Pending transfer, the transfer step did not complete.
Step 6: retention on the Storage Box
transfer.sh only uploads. It does not delete old bundles on the remote side. Either:
- Run a separate cron on the Storage Box with
find ./operaide -name 'full-backup-*.tar' -mtime +30 -deleteto drop bundles older than 30 days. Adjust+30to your retention policy. - Use Hetzner's snapshot or versioning features if you want point-in-time recovery on the Storage Box itself.
- Extend transfer.sh to delete remote bundles older than N days before uploading the new one. Keep this defensive: a script that can mass-delete remote backups is a footgun.
The Operaide host itself evicts old bundles automatically when /backups runs low (see OPERAIDE_BACKUP_MIN_FREE_BYTES in Reference). Local eviction is independent of remote retention.
Common mistakes
- Passphrase on the SSH key. Cron jobs cannot type passphrases. Use a passphrase-less key for unattended transfers, and protect the key file with
chmod 600. - Same key as your interactive operator account. The transport key has no passphrase. If it leaks, an attacker has push access to your backup target. Generate a dedicated key for transport only and remove its access first when rotating.
- Forgetting
--remove-source-filesand not touching.transferred. The bundle counts as not transferred and the UI flags it as transport-stuck after 7 days. - Wrong remote port. Hetzner Storage Box uses port 23 for SSH, not the default 22. The
-p 23flag is mandatory. - No retention. Storage fills up silently. Decide on a retention policy and implement it before the disk is full.
- No drill. Configuring transport is half the job. The other half is verifying you can restore from what landed on the Storage Box. See the Recovery Drill.