The Backup Strategy Every Self-Hosted SaaS Needs (3-2-1, Applied)
Cloud

The Backup Strategy Every Self-Hosted SaaS Needs (3-2-1, Applied)

3-2-1 is easy to nod along to and easy to get wrong in the specific way that only shows up on the day you need it. Here's what it actually means for one VPS running Postgres and Docker volumes, not the generic version you've already skimmed past twice.

Published September 13, 202611 min readUpdated Sep 6, 2026

Written by · Full-Stack Agentic AI Software Engineer — AI Agents, Automation & Revenue Systems for GTM/RevOps teams

In brief

What does 3-2-1 backup actually mean for a single self-hosted VPS, and what's the difference between a real backup and something that only looks like one?

3-2-1 means three copies of your data, on two different kinds of media, with one copy stored somewhere other than the server itself — and for a one-VPS stack that translates to a concrete, checkable list: the live data on the VPS counts as copy one, a local or same-datacenter backup counts as copy two, and an object storage bucket in a different location counts as copy three, the offsite one. Not everything on the box needs backing up the same way — your application code and infrastructure config are reproducible from git and don't belong in this budget at all, while your database and user-uploaded files are the only things that can't be rebuilt from anything else. RPO and RTO have to be decided on purpose, as numbers, before an incident forces an answer on you. A snapshot of a live database taken with a filesystem copy is not a backup, because it can capture files mid-write with nothing to warn you. And a backup nobody has restored is a hypothesis, not a backup, until it's been proven on a separate box.

  • 3-2-1 means three copies, two kinds of media, one copy offsite — for a single VPS this maps to live data, a local/scheduled backup, and an object storage bucket in a different region
  • Application code, Docker Compose files, and infrastructure config are reproducible from git — backing them up the same way as irreplaceable data wastes the backup budget that should protect what can't be rebuilt
  • RPO (how much data you can afford to lose) and RTO (how long recovery can take) are decisions to make deliberately, in writing, before an incident — not numbers you discover during one
  • A filesystem-level copy of a running Postgres data directory is not a valid backup — only a real dump (pg_dump) or a coordinated base backup plus WAL archiving guarantees internal consistency
  • An untested backup is not a backup — the only way to know a restore actually works is to run it on a separate machine before the day you need it for real
  • Encryption matters for the offsite copy specifically, and the key has to live somewhere other than the server it's protecting, or losing the server also loses the ability to read your own backups

Evidence notes

PostgreSQL backup methods

pg_dump produces a consistent logical snapshot safe to run against a live database; pg_basebackup with WAL archiving is the correct filesystem-level method; a raw copy of a running data directory risks internal inconsistency with no warning.

Cloudflare R2 pricing

Standard storage at $0.015/GB-month with zero egress fees at any usage level and a 10 GB-month free tier, checked September 2026 — a natural offsite destination since restoring from it costs nothing beyond the storage already paid for.

Coolify instance backup scope

Coolify's built-in backup covers its own configuration, projects and deployment history, but explicitly does not cover application data, databases, or arbitrary volumes — those need a separate plan.

Continue with purpose

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.

  1. Provision a separate, disposable machine — not the server you're protecting.

  2. 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.

  3. 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.'

  4. Time the whole exercise, because that number is the only honest answer to 'how long would recovery actually take.'

  5. 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

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.

Would this restore, though

Five questions on the gap between 'a backup ran' and 'a backup would actually work.' Answers and reasoning at the end.

1 / 5
You take a plain filesystem copy — cp or tar — of a running Postgres data directory. Is that a valid backup?

Apply this article

How to turn insights into execution

A practical sequence for teams turning concepts into production outcomes.

backupdisaster recovery3-2-1 backupPostgreSQLself-hostingVPScloud

Audit your current state

Map the bottlenecks and constraints connected to the article’s core problem.

Choose one bounded change

Test the most useful recommendation on one workflow before widening the scope.

Measure what changed

Keep the parts that improve the work, document what failed, and make the next decision from evidence.

Next step

Need help applying this in your stack?

I can translate these patterns into a concrete implementation plan for your team.

Discuss implementationBack to blog

Replies usually within 24 hours.

Next Steps

Continue reading

What Happens If Your VPS Dies? My Disaster Recovery Plan

"The server died" isn't one scenario, it's at least five, and they don't require the same response. A disk failure and a compromised box both end with the same VPS gone, but only one of them means your backups might be compromised too. Here's the plan for each, written down before any of them happen, not after.

Navigate

The Simplest Production Architecture for a Bootstrapped SaaS

One box, a handful of managed pieces around the edges, and a very short list of things you're not allowed to build yet. Here's the architecture, priced out to $20 a month, and the exact signal that tells you when to add each thing you skipped.

Navigate

How to Back Up and Restore a Coolify Server Properly

Coolify's built-in backup covers less than most people assume, and a filesystem copy of a live Postgres data directory is not a valid database backup no matter how confident it looks in a file listing. Here's what actually has to be backed up, and a restore you've actually tried before you need it.

Navigate

Should Your Database Live on the Same VPS as Your App?

The pitch for co-location is real: no network hop, no egress bill, one box to back up. So is the failure mode — one OOM event takes the app and the database down together, because they were never separate to begin with.

Navigate

How I Self-Host PostgreSQL for My SaaS (and When I Wouldn't)

Running Postgres in a container is easy. Running it in a way that survives a redeploy, a full disk, and an eventual major-version upgrade is the actual job. Here's the setup, tuned against Postgres's own defaults, and the honest list of where managed wins outright.

Navigate

10 Mistakes That Break a Self-Hosted SaaS

None of these ten show up as a single dramatic outage. They show up as a disk that quietly fills, a rollback that turns out to be impossible, a backup nobody ever restored. Here's the mechanism behind each one, and the fix.

Navigate

How I Restore an Entire Server From Backup (And How Long It Takes)

The order matters more than people expect, and the answer to "how long will this take" isn't a number I can hand you — it's dominated by your database size, your download bandwidth, and one DNS setting most people only think to change after they needed to.

Navigate

Managed Postgres vs Self-Hosted: Supabase, Neon and a VPS Compared

Supabase bills you for compute whether anyone's querying it or not. Neon bills you almost nothing until someone is, then charges for the second it takes to wake up. A VPS charges you the same either way and hands you every operational job both of the others do for you. None of these is the right answer by default.

Navigate

My Rule for Deciding What to Self-Host and What to Keep Paying For

Self-hosting everything is a bad idea, and I can point to the exact service where I decided that on purpose. Here's the actual rule I use, not a survey of options — and the one counterexample that explains why the rule exists.

Navigate

My Complete Self-Hosted Stack for SaaS in 2026

Contabo, Coolify, Docker, Cloudflare, Postgres, R2, Resend, Uptime Kuma, Turborepo and Docker Hub. Here's every piece of the stack I actually run, what each one replaced, and why I picked it over the alternatives.

Navigate