Everyone running a self-hosted SaaS on a single VPS already knows the phrase 3-2-1. Fewer of them can tell you, specifically, which three things on their own server it refers to, because the phrase is easy to nod along to in the abstract and surprisingly easy to get wrong in a way that only surfaces the day it matters. This post is the concrete version: not '3-2-1 is a good rule,' but exactly what copy one, two and three are on a box running Docker, Postgres, and a handful of volumes, and exactly which parts of that box don't need protecting the same way at all.
What does 3-2-1 actually mean on one VPS?
Three copies of your data. Two different kinds of storage media. One copy kept somewhere other than the server itself. That's the whole rule, and the reason it's stated as three numbers instead of one sentence is that each number closes a specific failure mode the others don't. Two copies on the same disk both die when that disk dies. Two copies on two disks in the same server both die when the server itself is destroyed, seized, or the account is suspended. Only a copy that's physically and administratively separate from the original survives every failure that isn't a targeted attack on your backup provider specifically. Where the backup strategy every self-hosted SaaS needs (3-2-1, applied) meets a revenue team, the practical guidance lives with XenGrowth's revenue operations work.
On a single-VPS stack, map the three numbers onto what you actually have running. Copy one is the live data: your Postgres instance and your Docker volumes, as they exist right now, in production. Copy two is a local or same-provider backup — a scheduled dump or snapshot stored on the same VPS's disk, or a second Contabo volume, which protects against accidental deletion or a bad deploy but not against the VPS itself disappearing. Copy three is the offsite one: an object storage bucket, ideally with a different provider and a different physical region than the VPS itself, which is the copy that survives everything the first two don't.
Copy | What it protects against | What it doesn't |
|---|---|---|
1: Live data on the VPS | Nothing — this is the thing being protected, not a backup | Everything; it's the original |
2: Local/same-provider backup | Accidental deletion, a bad migration, application-level corruption | Disk failure, the VPS itself being lost, account suspension |
3: Offsite object storage | Everything copies 1 and 2 don't — provider outage, disk failure, account issues, regional disaster | A backup process that silently stopped running, or one nobody has ever restored |
What actually needs backing up — and what's just reproducible?
This split is worth doing explicitly, in writing, for your own stack rather than trusting a mental list, because the cost of getting it wrong runs in both directions. Back up everything indiscriminately and your backup job gets slow, expensive, and eventually someone quietly disables the parts that are annoying. Back up too little because you assumed something was reproducible when it actually wasn't, and you find out the gap exists during the restore, not before it.
Not everything on the box deserves the same budget of attention, and treating all of it identically is how backup jobs get slow and expensive enough that someone eventually turns them off. Split the server's contents into two categories: things that exist nowhere else, and things you could recreate from a source you already control. The XenGrowth resource library goes further into the operations side of this.
Irreplaceable, needs real backup: your Postgres database and every row in it, user-uploaded files sitting in Docker volumes or bind mounts, environment variables and secrets, TLS certificate state if reissuing under rate limits would be a problem
Reproducible from git, does not need backup budget: your application source code, your Dockerfiles and Compose files, your CI/CD configuration, your infrastructure-as-code definitions
Reproducible from a package registry or Docker Hub, worth noting but not backing up: the exact image versions your stack depends on, since those are pinned and pullable again as long as the tags still exist
That distinction matters for more than tidiness. Coolify's own instance backup covers Coolify's configuration, projects, and deployment history — genuinely useful, but it's protecting the reproducible half of the story more than the irreplaceable half. It does not reach into your applications' databases or arbitrary volumes, which is exactly the split this section is describing: the platform layer is mostly reproducible or lightly configured, the data layer is not, and conflating the two is the single most common way people believe they're covered when they aren't.
How much data can you afford to lose, and how fast do you need to be back?
RPO — recovery point objective — is the answer to 'how much data can I afford to lose,' stated as a duration: if your database backs up every six hours and the server dies five hours in, you've lost five hours of writes. RTO — recovery time objective — is 'how long can I be down,' stated the same way. Neither of these has a universally correct value, and that's exactly the problem: most people never write either number down, which means the real RPO and RTO of their stack is whatever their backup schedule happens to produce by accident, discovered for the first time during an actual incident.
Decide both on purpose. A solo project with low write volume and tolerant users can reasonably accept a 24-hour RPO and a same-day RTO — daily backups, restored when convenient. A SaaS with paying customers writing data continuously needs an RPO measured in minutes or low single-digit hours, which means WAL archiving or a much tighter dump schedule, not a nightly cron job. Write the numbers down somewhere that isn't only in your head, because the value of having decided them in advance is that during a real incident you're executing a plan, not negotiating with yourself about how bad this is allowed to be.
RPO and RTO also aren't independent of each other, and treating them like two separate knobs to turn as tight as possible misses the actual tradeoff: a tighter RPO usually means more frequent, more complex backup machinery (WAL archiving instead of a nightly dump), which itself takes longer to restore correctly and can push RTO in the wrong direction if the restore procedure isn't equally well-rehearsed. Decide both together, against what your business actually needs, not against what sounds most impressive on paper. XenGrowth on governed AI marketing workflows covers the AI agents and marketing automation side of this.
An RPO you never wrote down isn't a small RPO or a generous one — it's an unknown number that gets discovered for you, at the worst possible time, by whatever your backup schedule actually was.
Why a snapshot of a running database isn't a backup
This is the mistake that looks completely reasonable right up until it isn't. Postgres stores its data as a directory of files, and copying that directory — a filesystem snapshot, a tar, an rsync — feels equivalent to backing up any other folder on the box. It isn't, because Postgres is a live process writing to those files continuously, and a copy taken mid-write has no concept of transaction boundaries. Some files can be copied from one instant and others from a slightly later one, and the result is a data directory that can be internally inconsistent in a way the copy operation itself gives you no warning about.
The two methods that actually work are different from that and different from each other. pg_dump produces a logical, point-in-time-consistent snapshot safe to run against a database that's live and being written to — it locks nothing globally and captures a coherent view of the data as it existed at one moment. pg_basebackup, paired with WAL archiving, is the correct way to do a filesystem-level backup, because it coordinates with Postgres's own internal consistency mechanisms instead of copying blindly. Neither of those is the same operation as a bare `cp` or `tar` of a running data directory, no matter how similar the resulting file listing looks.
Why an untested backup is not a backup
A file landing in an object storage bucket on schedule proves the upload step of your backup process works. It proves nothing about whether the file it uploaded is actually restorable, because the ways a backup silently fails are exactly the ways that don't show up as an error: a dump that finished but captured a transaction in an inconsistent state, a retention policy that quietly deleted the one recovery point you needed, a credential that expired three weeks ago so every 'successful' backup since has been writing to a bucket you no longer have write access to and nobody looked. XenGrowth on building one SEO and GEO content system works through AI search, GEO and discovery in more operational detail.
Provision a separate, disposable machine — not the server you're protecting.
Pull the most recent backup from offsite storage, the same way you would during a real incident, not from a copy sitting conveniently on your laptop.
Restore it into a fresh instance, and run a query that would only return the right answer if the actual data came through — a specific row count, a known record — not just 'the process didn't error.'
Time the whole exercise, because that number is the only honest answer to 'how long would recovery actually take.'
Repeat this on a schedule, not once. A restore that worked in January and hasn't been checked since is a claim about January, not about today.
This is the same discipline whether you're testing a single database restore or rebuilding the whole server from nothing — the full walkthrough, including the order operations have to happen in and what actually determines how long it takes, is worth its own space rather than a summary here.
Where does encryption fit, and where does the key actually live?
Encrypt the offsite copy specifically — it's the one leaving your direct control and sitting on infrastructure you don't operate, which is exactly the copy where a provider-side breach or a misconfigured bucket permission would otherwise expose everything in it. The part people get backwards is where the decryption key lives: storing the key only on the same server the backup is protecting means that losing the server loses both the data and the only thing that can read your own backup of it. The key needs to live somewhere independent — a password manager, a separate secrets store, printed and locked in a drawer if that's genuinely the level of paranoia the data deserves — anywhere except exclusively on the box whose destruction is the scenario you're encrypting against in the first place.
Data category | Backup method | Where the encryption key lives |
|---|---|---|
Postgres database | Scheduled pg_dump (or pg_basebackup + WAL) to offsite object storage | Separate secrets manager, never only on the VPS |
Docker volumes / uploaded files | Scheduled rsync or restic to offsite storage | Same secrets store as the database key — one place to check, not two |
Environment variables / secrets | Periodic export, stored encrypted and separately | A password manager or equivalent, independent of the server entirely |
Application code / infra config | Git — already versioned, already offsite by nature of hosting on GitHub/GitLab | N/A — not sensitive in the same way, and already redundant by design |
Everything above assumes you already know what's actually running on the box and where it's exposed — a backup strategy protects a server you've already hardened, it doesn't substitute for that hardening. If you haven't gone through securing a fresh VPS before this, that's the piece that comes first, not after. And if the question underneath all of this is really 'what happens if the box is just gone,' the failure modes worth planning for specifically — provider outage, disk failure, a compromised server, an account suspension — each need a different answer, which is exactly what a real disaster recovery plan has to spell out rather than treat as one generic 'the server died' scenario.
Further reading from XenGrowth
The XenGrowth resource library — what you'll learn: how the commercial side of this work is run, across search, automation and revenue operations.
XenGrowth on governed AI marketing workflows — what you'll learn: how the teams running AI marketing agents keep them governed and measurable.
XenGrowth on building one SEO and GEO content system — what you'll learn: how search and AI-answer visibility get run as a single content system.
Where this work meets go-to-market
If the backup strategy every self-hosted SaaS needs (3-2-1, applied) is part of a growth programme rather than a standalone build, XenGrowth's work on go-to-market systems is the companion reading.
Five questions on the gap between 'a backup ran' and 'a backup would actually work.' Answers and reasoning at the end.











