Getting the backups off the server
A trip expense-splitting app runs as a handful of containers on one small server. Its backup scripts were written and tested, but they had nowhere off the server to write, so they never ran. This design picks where the copies live, installs the nightly schedule and adds the checks that say when a night goes wrong, without changing a line of the backup scripts.
A backup with nowhere to go
Two backup scripts already existed and were tested. They had never run once.
One script dumps the database; the other archives the receipt photos. Both are needed: receipts live on disk, outside Postgres, so a database-only restore brings back expenses whose photos point at files that no longer exist.
Both scripts insist on writing to a mounted, off-box directory and refuse to run when it isn't there. That is deliberate. Creating the folder on the fly would quietly turn an unplugged mount into a plain folder on the server's own disk, and every night would look like a success. With no off-box storage chosen, the schedule was never installed.
The rule this design is built around
"An unrestored backup does not count as a backup." From the project's own backup runbook.
The system, and what this adds
Everything production runs: the containers, the jobs on the server's schedule and the services the app calls. Green outlines are new; amber is changed. Click a box for what it does.
connections (24)
- Trip members → DNS provider look up the domain
- Trip members → Caddy HTTPS
- Caddy → Expense app reverse proxy
- Expense app → Postgres queries
- Expense app → Uploads volume store receipt files
- Expense app → OAuth provider sign-in
- Expense app → FX rates API FX rates
- Expense app → Document-reading API read a document
- Container registry → Expense app image pull
- Host cron → Ledger check 03:15 nightly
- Ledger check → Postgres read-only check
- Host cron → Receipts prune 03:45 nightly
- Receipts prune → Uploads volume remove orphaned files
- Database dump → Postgres pg_dump in the container
- Receipts archive → Uploads volume tar the volume
- Restore drill → Postgres restore to a scratch DB
- Host cron → Database dump 04:00 nightly+ added
- Host cron → Receipts archive 04:05 nightly+ added
- Host cron → Restore drill monthly drill+ added
- Database dump → Off-box mount write .partial, rename+ added
- Receipts archive → Off-box mount write archive+ added
- Restore drill → Off-box mount read newest dump+ added
- Off-box mount → Backblaze B2 bucket encrypted upload (async)+ added
- Host cron → Heartbeat monitor ping after each job (async)+ added
Where the copies live
Every candidate can be mounted with rclone, so the scripts stay exactly as they are. The choice came down to four questions.
What does it cost? Could a hacked server delete the backups? Could one company's bad day take out both the app and its backups? And what does a restore cost? Scores are out of 100, and higher is better on every axis.
Backblaze B2
The cheapest per GB of the three, free at today's size, and the only one whose delete protection could be confirmed on the vendor's own docs. It also sits with a company that runs neither the server nor the domain.
- Object Lock, opt-in per bucket, no extra fee
- First 10 GB free, then $6.95 per TB
- Free downloads up to 3x the data stored
- Needs rclone; no native mount
- Region list not confirmed (one upload a night, so distance doesn't matter)
Cloudflare R2
Free to download from, and free at today's size. But its delete lock could not be confirmed in this review, and past the free tier it costs about twice as much as B2.
- No download fees at all
- First 10 GB a month free
- Lock not confirmed in this review
- $0.015 per GB after the free tier, about twice B2
AWS S3
Next door to the server, with mature Object Lock and versioning, at the highest storage price of the three. Downloads past the free 100 GB a month cost $0.12 per GB.
- Object Lock and versioning
- A region beside the server
- First 100 GB of downloads a month free
- $0.025 per GB a month, the highest here
- More access-policy setup than the other two
What storage costs once the free tier runs out
At today's size, under 10 GB, B2 and R2 cost nothing and S3 is 2.5 cents a GB. List prices are from each vendor's own pricing page, checked in October 2026.
Also considered
The server host's own object storage has a nearby region and object lock, but it is the same company as the server: one account suspension or billing failure would take the app and its backups together. Hetzner's Storage Box offers SFTP and built-in snapshots, but its current price couldn't be confirmed, so it is left off the chart.
One night, in order
The order is on purpose: check the ledger, prune, then copy, so the archive never carries files that were about to be deleted.
The rules it keeps
Four rules were already in the code and the design keeps them. Three are new.