by Rogerio Taques

How I Set Up Server Backups to Cloudflare R2 (And the Lessons I Learned Along the Way)

R Rogerio Taques
· · 5 min read

Server Room

The Problem

I manage several servers — a few VPS instances on Linode and a Raspberry Pi tucked away at home. For a while, I relied on restic backups running locally on each machine. The backups worked fine, but they were stuck on the same hardware they were protecting. If a VPS provider had an outage or my Pi's SD card failed, my backups would go down with them.

I needed offsite backups. Something durable, affordable, and easy to set up.

That's when I discovered Cloudflare R2.

Why Cloudflare R2?

R2 is Cloudflare's S3-compatible object storage. No egress fees, tight integration with Cloudflare's network, and a generous free tier. For someone running backups across multiple low-cost VPS instances, it sounded perfect.

The plan was straightforward:

  1. Install restic on each server
  2. Configure each server to back up to an R2 bucket
  3. Set up a dashboard to monitor everything from my local machine

Simple, right?

The First Hurdle: Restic and R2 Timeout Issues

I configured restic on my Raspberry Pi, pointed it at the R2 endpoint, and kicked off the first backup. After a few minutes, it timed out.

The Pi sits behind a residential ISP connection. Upload speeds are modest, and restic's default timeout settings weren't patient enough for the initial large upload. Even on the VPS instances with better connectivity, the backups were sluggish.

After some research, I realized this is a common pain point. Restic's S3 backend doesn't always play nice with high-latency or low-bandwidth connections, especially when dealing with large initial snapshots.

I started looking at alternatives:

  • rclone — simpler, great for syncing, but lacks restic's deduplication
  • kopia — modern, better timeout handling, but newer and less battle-tested
  • borgbackup — mature and efficient, but doesn't natively support S3

The trade-offs weren't obvious. I decided to stick with restic for now and optimize the configuration rather than switch tools entirely.

Lesson learned: Before committing to a backup tool, test it with your actual network conditions. A tool that works great on a fiber connection might choke on a residential ISP.

Building a Dashboard

With four servers to monitor, I needed visibility. A quick cron job running restic snapshots on each machine wasn't cutting it — I wanted a single view showing the health of all backups.

I wrote a bash script that SSHs into each server, pulls the latest snapshot info, and displays a formatted status report. The first version was simple: check each server sequentially, parse the output, print the results.

It worked, but it was slow. Each SSH connection took a few seconds, and running them one after another meant waiting 20+ seconds for the full report.

The Dashboard: Lessons in Bash

The dashboard script went through several iterations. Here are the issues I ran into:

Timezone Handling

The servers are spread across different regions, but I wanted all timestamps displayed in JST (Japan Standard Time). Converting between timezones in bash isn't as straightforward as you'd think.

The solution was simple: TZ=Asia/Tokyo date '+%Y-%m-%d @ %H:%M'. But getting there required debugging why date -d was returning unexpected results on some systems.

Bash Arithmetic Gotchas

I wanted to display the total backup size across all servers. The sizes come back in different units — GiB, MiB, sometimes TiB. Converting and summing them in bash led to some frustrating errors.

The issue? Bash's (( )) arithmetic evaluation only works with integers. When you pipe bc output (which includes decimals) into (( )), bash tries to execute "GiB" as a command. The error message was cryptic:

./backup-dashboard.sh: line 123: GiB: command not found

The fix: use [ ] string comparison instead of (( )) for floating-point comparisons with bc.

Parallelization

The biggest performance win came from running all SSH checks in parallel. Instead of:

check_server "VPS-1" ...
check_server "VPS-2" ...
check_server "VPS-3" ...
check_server "RPi" ...

I used background processes:

check_server "VPS-1" ... &
check_server "VPS-2" ... &
check_server "VPS-3" ... &
check_server "RPi" ... &
wait

Each server's output goes to a temp file, then they're displayed in order after all complete. The dashboard now runs in roughly 1/4 of the time.

Pro tip: When running background processes that write to files, use mktemp -d and trap the cleanup. Don't hardcode temp file paths — you'll regret it when two instances run simultaneously.

What's Next

The dashboard is working. The backups... are still a work in progress. I'm experimenting with restic's --upload-speed and --keep-last options to make the Pi's uploads more manageable.

I'm also considering splitting the strategy: use rclone for quick file syncing to R2, and keep restic for the deduplicated, versioned snapshots. Sometimes the best solution is a combination of tools rather than a single Swiss Army knife.

Raspberry Pi

Key Takeaways

  1. Test with real network conditions. What works on a fast connection may timeout on a slow one.
  2. Don't fight your tools. If restic's S3 backend doesn't suit your network, consider rclone or kopia.
  3. Build visibility early. A dashboard saves you from checking each server manually.
  4. Parallelize SSH calls. Your future self will thank you.
  5. Bash has quirks. Floating-point arithmetic and timezone handling will bite you eventually.

Backing up to the cloud isn't rocket science, but it's rarely as simple as the documentation suggests. The key is to start small, monitor closely, and iterate.

Rogerio is a seasoned Full-Stack Engineer, Product Owner, and Entrepreneur with a deep background in Information Systems. He bridges the gap between complex technical stacks and a customer-centric, product-first mindset.