Backing Up a Solo-Dev VPS to Cloudflare R2 with Restic (Without a Surprise Bill)
If you run one VPS with no ops team behind you, your backup strategy is probably a cron job, a tarball, and a vague sense of dread about where the tarball actually goes. Restic plus Cloudflare R2 fixes that cheaply — encrypted, deduplicated, versioned backups shipped off-server, for well under a dollar a month on a typical app server. But most guides stop at "here are the commands" and skip the part that actually matters for a solo dev: what does this cost once it's running for real, and where does the bill quietly grow?
This walks through the setup, a real cost breakdown for a 20–30 GB Laravel/WordPress-style VPS, and the one workload pattern (lots of small files) where R2's pricing model stops being basically free.
Why restic + R2, specifically
Restic is a backup tool that chunks your files, deduplicates the chunks, encrypts everything client-side, and uploads only what's changed since the last run. It speaks the S3 API natively, so it can write straight to any S3-compatible bucket — no separate sync tool needed.
Cloudflare R2 is the destination that makes this specific combination attractive for a single small server: it's S3-compatible (so restic just works), and unlike AWS S3, Google Cloud Storage, or Azure, R2 charges nothing for egress — you only pay to store data and to perform read/write operations. This was the entire point of R2 when Cloudflare launched it:
Announcing Cloudflare R2 Storage: Rapid and Reliable Object Storage, minus the egress fees. https://t.co/U2dVhVSb2U #BirthdayWeek🎂
— Cloudflare (@Cloudflare) September 28, 2021
For a backup workflow specifically — where you occasionally pull a large restore but otherwise just push small incremental writes — removing the egress line item removes the one cost on other providers that's hardest to predict in advance.
If you want the two-minute version of how R2 works before touching a terminal, Cloudflare's own intro covers buckets, storage classes, and the zero-egress model directly:
Setting it up
You'll need an R2 bucket and an API token scoped to it — not your global Cloudflare account token.
- In the Cloudflare dashboard, go to R2 Object Storage → Create bucket. Name it something specific, like
vps-backups-appserver. - Under R2 → Manage API Tokens, create a token with Object Read & Write permission scoped to that one bucket only.
- Install restic on the VPS:
sudo apt update && sudo apt install -y restic(or grab a newer release directly from restic's GitHub if you need a recent feature). - Store your credentials somewhere restricted, e.g.
/root/.config/restic/r2.envwith mode600:
export RESTIC_REPOSITORY="s3:https://<ACCOUNT_ID>.r2.cloudflarestorage.com/vps-backups-appserver"
export RESTIC_PASSWORD_FILE="/root/.config/restic/repo-password"
export AWS_ACCESS_KEY_ID="<R2_ACCESS_KEY_ID>"
export AWS_SECRET_ACCESS_KEY="<R2_SECRET_ACCESS_KEY>"
export AWS_DEFAULT_REGION="auto"
That last line matters: restic's S3 backend defaults to us-east-1 and expects a region to sign requests, but R2 doesn't have regions in the AWS sense. Leaving AWS_DEFAULT_REGION unset or setting it to auto avoids signing errors during init.
source /root/.config/restic/r2.env
restic init
restic backup /var/www /etc/nginx /home/deploy --exclude=/var/www/*/storage/logs
For a database-backed app, dump the database to a file first and include the dump in the backup path — restic backs up files, not live databases:
mysqldump --single-transaction mydb | gzip > /root/db-backups/mydb-$(date +%F).sql.gz
restic backup /var/www /root/db-backups
Automate it, then prune it
A cron entry that runs nightly and forgets old snapshots on a retention schedule:
0 3 * * * . /root/.config/restic/r2.env && restic backup /var/www /root/db-backups && restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune >> /var/log/restic-backup.log 2>&1
Two things worth knowing before you rely on this:
- Lock files after a crash. If a backup dies mid-run, restic leaves a lock in the repository and the next run refuses to start with "unable to create lock in backend." Add
restic unlockas a safety step beforebackupin the cron job if this server has ever had a mid-backup reboot. - Test the restore, not just the backup.
restic checkvalidates repository integrity but doesn't prove your data comes back. Runrestic restore latest --target /tmp/restore-teston a schedule (monthly is reasonable for a solo project) and actually diff a few files or re-import the SQL dump.
What this actually costs
R2's published rates, straight from Cloudflare's pricing page: Standard storage is $0.015/GB-month, Class A operations (writes, lists — the state-changing calls) are $4.50 per million, Class B operations (reads) are $0.36 per million, and egress is free at any volume. There's also a permanent free tier: 10 GB storage, 1 million Class A operations, and 10 million Class B operations per month.
Take a realistic single-server scenario: a Laravel or WordPress app with a 20 GB /var/www directory, daily database dumps, and a 7-daily / 4-weekly / 6-monthly retention policy.
- Initial backup: restic's default chunk size averages around 1 MB, so a 20 GB first backup produces on the order of 20,000–25,000 objects — that's 20,000–25,000 Class A operations, a one-time cost, and still 2–3% of the monthly free allowance.
- Daily incrementals: restic only uploads new or changed chunks. A typical day of app + log + DB-dump changes on a small server might be 200–500 MB of new data, or roughly 200–500 Class A operations per day — well under 15,000/month.
- Storage, steady state: with pruning enabled, retained snapshots for a 20 GB app usually settle around 25–35 GB of unique stored data (some historical deltas persist until they age out of retention).
Total Class A operations for the month land somewhere around 30,000–40,000 — nowhere near the 1 million included free. Storage of 30 GB costs 30 × $0.015 = $0.45/month. Class B operations (mostly from the occasional integrity check or restore test) stay inside the 10 million free reads. Total: under $0.50/month, plus whatever you're already paying for the VPS itself.
The hidden cost: lots of small files change the math
That estimate holds for an app directory and database dumps — a few thousand files, most of them not changing day to day. It does not hold if you're backing up something with hundreds of thousands of small files that mutate frequently: a WordPress uploads directory that regenerates thumbnails on every image edit, a cache directory with high file turnover, or a Laravel storage path full of small session or queue files you forgot to exclude.
Restic issues roughly one Class A operation per new or changed chunk, and small files each become at least one chunk regardless of size. Back up a directory with 500,000 small files that change often, and a single run can burn hundreds of thousands of Class A operations. At that volume you can clear the 1-million free allowance within a few backup cycles, and pay $4.50 per additional million — still cheap in absolute terms, but no longer the "basically free" number most guides quote.
The fix is the same one good backup hygiene requires anyway: exclude caches, logs, and regenerable derived files (thumbnails, compiled assets, session stores) from the restic backup path, and only back up source data — uploaded originals, code, config, and database dumps — that you can't regenerate.
Who this setup is for
Solid fit if you're running one to a handful of VPS-hosted apps, want off-server backups without paying for a managed backup product, and are comfortable with a bit of cron and shell. It's also a reasonable choice if your upload traffic is bursty and unpredictable, since there's no egress fee to worry about on a large restore.
Look elsewhere if your server sits behind a slow residential-grade uplink for the initial backup — restic's S3 backend has been reported to time out on large first uploads over low-bandwidth or high-latency connections; running the first backup over a wired connection or seeding it while temporarily on better bandwidth avoids the headache. And if you're backing up a large, high-churn media library rather than an app server, budget for Class A operations explicitly rather than assuming the free tier covers it.
FAQ
Do I need to set a region for R2 like I would for AWS S3?
No real region exists on R2, but restic's S3 backend still needs one to sign requests. Set AWS_DEFAULT_REGION=auto (or leave it unset) to avoid signing errors.
Does restic encrypt data before it reaches R2?
Yes. Restic encrypts and deduplicates locally before upload; the key is derived from your repository password, which Cloudflare never sees. Store that password somewhere separate from the server itself — if the VPS is compromised and the attacker has the password file, they can read the backups.
What happens if I go over the R2 free tier?
You're billed per the published rates for whatever exceeds the free allowance — $0.015/GB-month storage, $4.50/million Class A ops, $0.36/million Class B ops. There's no hard cutoff or service interruption; it's straightforward overage billing.
Can I use this same setup for multiple servers?
Yes — either one bucket per server (cleanest for scoping API tokens and tracking cost per server) or one bucket with a distinct RESTIC_HOST tag per machine. Separate buckets make it easier to see which server is actually driving your R2 bill.
Is R2 the cheapest place to store backups specifically?
Not necessarily on raw storage price — Backblaze B2 and some other providers charge less per GB stored. R2's advantage for backups is the combination of S3 compatibility (restic works with zero extra tooling) and zero egress, which matters most when you actually need to pull a full restore.
Bottom line
For a single app server with a normal file count, restic and R2 together land at well under a dollar a month, and the setup is a handful of environment variables plus a cron entry. The part worth remembering is that R2's pricing is friendly to storage and reads but meters writes — so the backup path you point restic at matters as much as the provider you choose. Exclude what you can regenerate, back up what you can't, and the free tier covers a solo project comfortably.
Sources: Cloudflare R2 official pricing; Cloudflare R2 S3-compatible API docs; Cloudflare Learning Paths – Getting started with R2; restic official documentation. Pricing verified against Cloudflare's live pricing page on August 26, 2026 — confirm current rates before relying on this for budgeting.
Comments 0
Be the first to comment.
Leave a comment