RAFAEL REIS case study
platform engineering · high availability

A one-person platform team

TL;DR

Stack: Proxmox · Docker · GitOps control plane · PostgreSQL/Patroni · MongoDB replica sets · Redis Sentinel · Traefik · Cloudflare · GitHub Actions

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 shape of the fleetinteractive: hover over a tenant
TRAEFIK · routing by domain dealer-domain.com → the tenant's containers tenant 1 site · api backoffice tenant 2 site · api backoffice tenant 3 site · api backoffice ×60+ PostgreSQL · MongoDB · Redis shared data plane · per-tenant isolation in the application layer HA cross-DC (see the migration case)
one template, versioned · new tenant = 1 entry in the manifest

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:

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:

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.

the fleet, materialized from the manifest
tenants: 60+ · containers: ≈200 · external probes: 180+
each chip = 1 dealership (site · api · backoffice) from the same versioned template · new tenant = 1 entry in the manifest

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:

layered backupshover: what each layer covers
live replica cross-DC RPO seconds local ZFS snapshots, every 15 min RPO 15 min daily encrypted VM backups · restore timed at ~20 min RPO 24h off-site to 2 destinations · immutable object-lock + hourly logical dump of the revenue database catastrophe RPO 1h
each layer covers the failure the one above it doesn't · a replica is not a backup

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

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