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 also 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 with an 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, 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 stops being a procedure and becomes 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. 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. 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 it 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