A one-person platform team
TL;DR
- Context: B2B2C automotive SaaS, lean and bootstrapped. Tech team: 1 product dev — and me, owning 100% of the platform and the infrastructure.
- Scale: 60+ tenants in production (≈200 containers), two data centers in hot-standby, four-nines target.
- The thesis: one person runs this not through heroics but through architecture: everything declarative, everything observable, everything with a way back.
The context: when the platform team is you
In a bootstrapped SaaS there is no "open a ticket with infra". I am the infra — and the SRE, the DBA, the security team and the on-call rotation. That changes the calculus of every technical decision: every component that enters the architecture is a component I will patch, monitor, and get woken up for.
The discipline that emerges from this has a name: minimal surface area. It is not aesthetic minimalism — it is survival. The sections below are the same decision applied at different layers.
The shape of the problem: 60+ tenants, ≈200 containers
Each tenant — a dealership with its own domain — is materialized as its own set of containers (site, API, backoffice), all generated from the same versioned template, on top of a shared data plane (PostgreSQL + MongoDB) with per-tenant isolation in the application layer. Routing is by domain, at the reverse proxy, and DNS for the entire fleet is automatically reconciled against an anchor record — a new tenant goes into a manifest, and the rest just happens.
The trade-off is deliberate: per-tenant containers cost more memory than a pure multi-tenant monolith, but they buy failure isolation (one tenant going haywire doesn't take down its neighbors), deploy and rollback per tenant, a fleet a single human can reason about — and simplicity at the edge: each dealership uses its own commercial domain pointing straight at its container set, with no complex proxy-and-rewrite layer multiplexing a single app across dozens of third-party domains.
High availability that doesn't depend on me being awake
Production runs in two geo-separated data centers in hot-standby: PostgreSQL with Patroni (auto-failover), MongoDB replica sets with an arbiter at a neutral third site, Redis Sentinel, asynchronous replicas — RPO of seconds. In front, a load balancer whose health check only returns 200 in the data center where the database is the writable leader: traffic follows the database's failover on its own, with detection in ~1–2 minutes, no human intervention.
The full story of how production got to these two DCs — with zero downtime — is the migration case. Here, the point is a different one: automatic failover is what lets a team of one sleep.
The internal platform: GitOps without ceremony
Fleet deploys are governed by GitOps, via a lightweight control plane — an open-source tool adopted on purpose (central manager + per-VM agents, zero-trust network). Minimal surface area applies here too: I don't reinvent commodity; the bespoke engineering goes where off-the-shelf tools don't reach — the manifest that materializes the fleet, the DNS reconciler, the failover pre-warmer and the bespoke observability:
- Desired state in git. Deploy is commit + push; the control plane only applies what has a new commit. Drift detection compares what is running against what git says should be running.
- Adoption without recreation: existing containers can be adopted into the declared state with no downtime.
- The control plane doesn't manage itself. Explicit guard: the pieces that hold it up (the control plane itself, the edge) only come up via direct access — because the tool that solves your problem can't depend on itself to get fixed.
- Secrets out of git: self-hosted manager, with least-privilege machine identities per VM.
Before this there was a point-and-click container management UI. I replaced it because a UI has no diff, no history, no revert — git gives you all three for free.
CI on GitHub Actions (multi-arch builds), images in a private registry, and the tenant fleet provisioned from a manifest + versioned scripts on a timer — a new tenant is not a procedure, it's an entry in a file.
Why NOT Kubernetes (yet)
The question everyone asks. The short answer: Kubernetes solves organization-scale problems I don't have, at an operating cost I would pay alone.
The long version: what I need from an orchestrator is desired state, reconcile, drift detection, rollback and routing — and the GitOps control plane + Docker + reverse proxy deliver that with a fraction of the moving parts. The Kubernetes control plane is, itself, a distributed system someone has to operate, upgrade and understand at 3 a.m. — and here that someone has a first and last name. My fleet is stable and predictable (tenants have no Black Friday elasticity), so dynamic scheduling and autoscaling — the heart of K8s's value — would sit idle.
And the genuinely hard problems in this operation — cross-DC database failover, replication, DNS/LB cutover, immutable backup — live below and above the orchestrator. Kubernetes wouldn't solve any of them for me; I would just end up operating them and Kubernetes.
The "(yet)" is honest: at larger scale, or with a team, the math changes — and knowing exactly which problem K8s would solve for me (because I solved each one by hand) is precisely what lets me know when to adopt it.
By the same principle, no Terraform and no Ansible: VMs are born from a cloud-init template + versioned scripts in the repo, straight against the hypervisor's API. For 2 hypervisors and a dozen VMs, an extra declarative layer is more surface to maintain — the decision, recorded in an ADR, was "one way to manage things, less surface". IaC is the principle; the tool gets chosen by the size of the problem.
Observability: the eyes that replace a team
Here it was the inverse of the control plane: built bespoke, because generic observability doesn't know what a tenant is. If one person runs the fleet, the fleet has to report on itself:
- Metrics across the whole fleet (hosts, containers, Postgres/Patroni, Mongo, Redis, connection pooling), with months of retention and ~100k active series.
- 180+ external probes hitting the tenants' sites from the outside — the availability that gets measured is the one the customer sees, not the one the server believes.
- Structured logs from the entire fleet in a central pipeline — millions of lines/day with severity classification.
- 30+ alert rules routed by criticality (critical wakes me up; the rest can wait for coffee) and crossed deadman switches: the system that monitors is monitored by another — who watches the watcher is a question with a written answer.
And the probe list generates itself from the tenant manifest: a new tenant enters monitoring without anyone remembering to add it. Automation that depends on human memory is not automation.
Layered backups (because a replica is not a backup)
An asynchronous replica protects against hardware loss — and faithfully replicates your bad
DELETE. Hence, independent layers:
- Live replica cross-DC — RPO of seconds, for loss of a site.
- ZFS snapshots every 15 minutes — for the "oops" from a moment ago, instant rollback.
- Daily encrypted backup of the entire VMs, deduplicated, with restores tested and timed (hundreds of GB restored in ~20 minutes).
- Off-site to two independent destinations — another data center and another provider — the latter with immutable object-lock: not even ransomware holding my credentials can delete it. And the revenue database gets a logical dump every hour: catastrophe RPO dropped from 24h to 1h.
The bill at the end of the month
All of this — two data centers, real HA, observability, immutable backups — runs on two bare-metal servers and a handful of cheap managed services, at a fraction of what the same footprint would cost in managed cloud. Hot-standby instead of active-active was a deliberate cost decision: half the hardware sitting "idle" is the insurance premium — and there is still headroom for the fleet to double.
Cost optimization here is not dashboard FinOps: it's architecture that fits the business.
Lessons
- Minimal surface area is a strategy, not an aesthetic. Every component you don't have is one less patch, one less alert and one less failure mode — pick boring technology and operate it well.
- Automation as survival. A team of one doesn't scale through effort; it scales by not needing to be present. If a process depends on human memory, it has already failed — it just hasn't told you yet. (The corollary: when 200 containers decided to restart at the same time after a reboot and drowned the databases, the answer was not "restart more carefully next time" — it was a staggered, health-check-gated bring-up, at boot, written once.)
- Full ownership teaches what abstraction hides. Solving failover, replication and DR by hand is what tells me exactly what I would be buying from an orchestrator or a cloud — and when it's worth buying.
- Treat the lab like production and production like code. The operating pattern that runs the SaaS — git as the source of truth, lightweight agents, zero-trust — was proven first in my homelab: 12 years of lab became method, not hobby.
Other case studies
Migrating 50+ live tenants across data centers, with zero downtime Hot replica, atomic cutover, and proof by packet capture, not optimism. 12 years of homelab: my self-hosted AI stack Local LLMs on a GPU, hybrid RAG, and an agent that pentests my own SaaS.Always happy to compare notes with people who run real infrastructure.
LinkedIn