Weekly automated backup pipeline: two source servers → a Docker host running n8n and rclone → Cloudflare R2. Each host's backup runs independently, with Telegram notifications on success (including bytes uploaded and total size in R2) and on failure.
For each host:
- Mount — SSHes into the source host and mounts its local backup
drive read-only by UUID at
/mnt/backup-sync(idempotent: skips if already mounted, so a re-run never double-mounts). - Start sync — SSHes into the Docker host and launches a detached
(
docker run -d) rclone container that syncs the source host's/mnt/backup-sync/...folder (read over SFTP) to an R2 bucket. Detached on purpose: once launched, nothing stays attached to the container, so restarting n8n (e.g. an unattended Watchtower update) can't interrupt an in-progress transfer. - Wait for sync —
docker waits for that container, dumps its logs, removes it, and exits with its real exit code — so the rest of the workflow sees the transfer's actual result even though it ran detached. - Unmount — unmounts the source host's backup drive.
- Check Size — queries the R2 bucket's total size for this host via
rclone size --json. - Combine Result — a Code node that determines success/failure and formats human-readable byte counts (uploaded this run, total in R2).
- IF → Notify / Alert — sends a Telegram message either way.
| Credential | Type | Used by |
|---|---|---|
| SSH: host-a | SSH Private Key | Mount/Unmount nodes for host A |
| SSH: host-b | SSH Private Key | Mount/Unmount nodes for host B |
| SSH: docker host | SSH Private Key | Start sync / Wait for sync / Check Size nodes (all run on the Docker host) |
| Telegram account | Telegram API | Notify/Alert nodes |
n8n never stores these secret values in the workflow JSON — only an internal
credential id + label. That's why this file is safe to publish, but you
still need to create the four credentials above yourself and re-point each
placeholder node at them after import (REPLACE_ME_1, REPLACE_ME_2,
REPLACE_ME_3, REPLACE_ME_TG).
Import startos-backup-to-r2-TEMPLATE.json into n8n, then:
- Re-point every SSH node's credential (see table above).
- Re-point both Telegram nodes' credential.
- In both
Notify:andALERT:nodes, replaceREPLACE_WITH_YOUR_TELEGRAM_CHAT_IDwith your chat ID (message @userinfobot on Telegram to get yours). - In both
Mount:nodes, replaceREPLACE_WITH_DISK_UUID_HOST_A/_HOST_Bwith the real UUID of each source host's backup drive (lsblk -fon that host, orblkid). - In
build_workflow_public.py,R2_BUCKET, the twosftp_remotevalues, anddest_foldervalues are placeholders (host-a,host-b,REPLACE_WITH_YOUR_R2_BUCKET) — either edit the script and regenerate, or just edit the relevant SSH node commands directly in the n8n UI to match your own rclone remote names and bucket.
-
/opt/rclone/rclone.conf— holds your R2 access key/secret (as ther2remote) and your SFTP remotes' pinned host keys. Never commit this file. -
/opt/rclone/.env— env vars passed to the rclone containers (also holds R2 credentials if you split them out this way). Never commit this file either. If you want an example in the repo, commit a.env.examplewith placeholder values and add the real.envto.gitignore. -
Pin each SFTP remote's host key once (silences rclone's "no host key validation" notice and makes future connections actually verify the host, instead of just trusting it — worth doing before your first real run):
docker run --rm --env-file /opt/rclone/.env -v /opt/rclone:/config/rclone \ rclone/rclone:latest --sftp-pin-host-key lsd <your-remote-name>-sftp:
/opt/rclone/.env
/opt/rclone/rclone.conf
*.env
(Adjust paths if you're committing this from inside /opt/rclone itself
rather than a separate repo — the point is: the workflow JSON is safe to
publish, the rclone config/env files next to it on the Docker host are not.)