Every architecture diagram you'll find for a SaaS that supposedly has zero users looks like it's already been acquired by a company with a platform team. Load balancers fanning out to auto-scaling groups, a message queue between every two boxes, a services layer talking to a services layer. None of it is wrong, exactly — it's just wrong for you, right now, and copying it is how a two-person team ends up spending its runway on Kubernetes YAML instead of the product. The architecture that actually gets a bootstrapped SaaS to its first hundred paying customers is much smaller than that, and it fits on one box.
What actually has to run to serve real traffic?
Three things, and only three: your app, running as a container; Postgres, running as another container next to it; and a reverse proxy in front of both, handling TLS and routing. That's the entire compute layer. If you're running Coolify on top of it, the reverse proxy is Traefik by default and you never touch its config directly — you point a domain at the box, Coolify issues the certificate, and you're done. If you're running plain Compose, Caddy does the same job in about fifteen lines. Either way, the reverse proxy's job is boring on purpose: terminate TLS, forward the request, don't get creative. If you are scoping the simplest production architecture for a bootstrapped SaaS for a business rather than a codebase, XenGrowth's revenue operations work covers that angle.
This is the part people skip past to get to the interesting stuff, so it's worth saying plainly: a single well-specified VPS running these three containers can serve a genuinely meaningful amount of traffic for a SaaS that isn't a consumer app with millions of daily actives. Postgres on modest hardware handles thousands of transactions per second before it's the bottleneck. Your app container is very likely I/O-bound on the database, not CPU-bound on the box. The ceiling here is much higher than it looks from the outside, and almost nobody building a B2B SaaS actually hits it in year one.
What has to live off the box, and why each one specifically
Object storage — user uploads, generated PDFs, anything that isn't in Postgres — goes to Cloudflare R2, not the container's own disk. A container's filesystem isn't durable across a redeploy unless you've explicitly mounted a volume for it, and even then, a single disk is a single point of failure for the one class of data that's genuinely irreplaceable: what your users uploaded.
Transactional email goes to Resend, not a self-hosted mail server. Running your own MTA means owning SPF, DKIM, DMARC, and a sender reputation you have to protect from every other tenant on the same IP range if you're on a cheap VPS provider — a full-time job disguised as a Docker container. Resend's free tier covers 3,000 emails a month, capped at 100 a day, which is enough for a pre-revenue product's signup and password-reset flow with room to spare.
Backups go to object storage in a different provider account than the one running your app, on a schedule that runs whether or not you remember to trigger it. This is the one that gets skipped longest and hurts worst when it does — a backup sitting on the same disk as the database it's backing up survives every failure except the one that actually happens, which is the disk dying.
What you deliberately don't build yet, and the signal that tells you it's time
What you're skipping | Why it's not there yet | The signal that tells you to add it |
|---|---|---|
A message queue | Everything your app does today can happen synchronously inside the request, or as a cron job that runs against Postgres directly | You have a background task that must survive the process crashing mid-run — not "might be nice to retry," but a real correctness requirement |
A service mesh | You have one service. Mesh solves service-to-service traffic policy, retries, and observability across many services talking to many services | You have more than two services that need to discover and call each other, and you're already hand-rolling retry logic between them |
Kubernetes | One box is easier to reason about, patch, and debug than a cluster, and a cluster's baseline operational cost is fixed whether or not you use it | You need workloads to schedule across more nodes than one person can patch by hand, with independent scaling per service |
Read replicas | A single Postgres instance on decent hardware handles read load that would surprise most people, and replication adds its own failure modes to manage | Read queries are measurably slowing down writes on the primary, not "reads feel like they could be faster" |
Notice the shape of every signal in that table: it's something you can point to happening, not something you're forecasting might happen. "We might need to scale eventually" is not a signal. A slow query log showing writes queuing behind reads is. On the operations side of this specifically, The XenGrowth resource library is worth reading.
A $20/month architecture, priced out
Piece | Choice | Monthly cost (Sept 2026, Contabo's own pricing page) |
|---|---|---|
Compute (app + Postgres + proxy) | Contabo Cloud VPS 4 — 4 vCPU, 8 GB RAM, 100 GB SSD | €5.50 (promotional, first 24 months) |
Object storage + backups | Cloudflare R2, standard tier, first 10 GB free | $0.015/GB beyond 10 GB — roughly $1-2 at this scale |
Transactional email | Resend free tier — 3,000/month, 100/day cap | $0.00 |
DNS, CDN, WAF | Cloudflare free plan | $0.00 |
Total | roughly €6.50-7.50, about $7-8/month at current exchange rates |
That leaves more than half of a $20 budget completely unspent, which is worth sitting with for a second: the constraint on this architecture was never money. It's discipline about what you refuse to add. Two honest caveats on that number: it's in euros, converting to a dollar figure at whatever the exchange rate happens to be rather than a fixed price, and it's promotional for Contabo's first 24 months — worth knowing before you build a three-year cost model on it, not a reason to avoid the plan. If you want headroom before you need it, stepping up to Contabo's Cloud VPS 6 at €7.50/month (6 vCPU, 12 GB RAM, 200 GB SSD) still lands the whole stack comfortably under $20, with real RAM headroom before any of the deferred pieces above become relevant.
Do you need a whole observability stack to run this responsibly?
No, and this is another place people over-build early. A single box doesn't need a distributed tracing setup, a metrics warehouse, and a dedicated on-call rotation — it needs to answer three questions when something's wrong: is the app responding, is the disk filling up, and did the last backup actually succeed. All three are answerable with tools that already exist on any Linux box, plus a health-check endpoint your reverse proxy hits on a schedule. There is a longer treatment of AI agents and marketing automation in XenGrowth on governed AI marketing workflows.
An uptime check hitting a real health endpoint (one that pings Postgres, not just returns 200) from outside the box, so you find out about downtime from a monitor instead of a support ticket
Disk usage alerting at something like 80% full — a Postgres box that silently fills its disk with WAL segments or logs doesn't degrade gracefully, it stops accepting writes
A notification, even a simple one, when the nightly backup job's exit code isn't zero — the worst version of a backup failure is one nobody notices for three months
Basic log retention on the app and Postgres containers, rotated so a runaway error loop can't fill the disk on its own
That's the whole list. It's a cron job, a health check, and an alert channel — not a Prometheus and Grafana stack, and definitely not a reason to add a fourth container class to a box that's supposed to stay boring.
Where this breaks first, honestly
One box has exactly one failure mode that matters: the box goes away, and everything on it goes away with it, for however long it takes to bring up a replacement. That's the real tradeoff, and pretending otherwise would undercut the whole argument. What makes it acceptable is that the two things you actually can't regenerate — user data and uploaded files — are the two things this architecture already keeps off the box, in Postgres with WAL-backed backups and in R2 respectively. Losing the VPS itself is an inconvenience you fix by redeploying from the same Compose file onto a new box; losing your only copy of the database is the failure that actually ends a company, and this architecture is built specifically so that one can't happen quietly.
The architecture isn't simple because simple is a virtue. It's simple because every piece you add before you need it is a piece someone has to keep working, forever, for a problem that doesn't exist yet.
What if the app itself needs more than one instance?
This is a narrower question than "do I need Kubernetes," and it's worth separating the two. Running two or three replicas of the app container behind the same reverse proxy — for redundancy during a deploy, or to use more of the box's own CPU — doesn't require a scheduler or a second server. Compose and Coolify both support declaring more than one replica of a service on the same box, with the reverse proxy load-balancing across them. That's still firmly inside this architecture, not a step beyond it: Postgres is still one instance, the box is still one box, and nothing about the deferred list above becomes relevant just because the app tier has a few copies of itself running side by side. On AI search, GEO and discovery specifically, XenGrowth on building one SEO and GEO content system is worth reading.
Where it does become a step beyond this architecture is the moment those replicas need to live on more than one physical box — because now you have a scheduling decision to make about which box runs what, and that's a different problem than the one this post is solving. Until that's true, more app replicas on the same VPS is a config change, not an architecture change.
So what do you actually build, in order?
Provision one VPS and put a reverse proxy in front of it — Traefik through Coolify, or Caddy directly, either is fine, the point is TLS and routing handled once, correctly
Run Postgres as a container with a named volume, not the default anonymous one — this single detail is the difference between data surviving a container recreation and quietly not
Point object storage at Cloudflare R2 from day one, even before you have real users uploading anything, so the code path never touches local disk
Wire transactional email through Resend's API on day one — there's no version of this where self-hosting email pays off earlier
Set up an automated, off-site backup job against Postgres before you have data you can't afford to lose, not after
Everything else on the deferred list stays deferred until its specific signal shows up in a log, a query plan, or an actual incident — not a planning meeting
This is also the reason the database deserves its own post rather than a paragraph here — the anonymous-volume trap alone has cost people their entire dataset, and tuning how I self-host PostgreSQL for my SaaS matters more than which VPS you picked. And if the question you're actually sitting with is the Kubernetes-shaped one, do you really need Kubernetes for a small SaaS walks through that threshold directly rather than as a single row in a table. Where this architecture fits against the wider tooling landscape — Compose, Coolify, or Kubernetes — is its own three-way argument, made in full here.
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 simplest production architecture for a bootstrapped SaaS is part of a growth programme rather than a standalone build, XenGrowth's work on go-to-market systems is the companion reading.
Four questions on the components people add early and rarely need. Each one is a real tool solving a real problem — the question is whether it's your problem yet.












