Skip to main content

A Full PaaS on One $11 ARM Server: What Real CAX21 Usage Data Says About Tenant Capacity

13 min readDora NodaDora Noda
Share
On this page

A managed Kubernetes control plane on AWS costs about $73 a month before it runs a single workload. Add a load balancer, a managed Postgres instance, and a worker node, and you are past $300 a month before your first tenant deploys. So when a developer publishes the actual kubectl top output from a multi-tenant deployment platform running on a single €10.99 Hetzner box — API server, dashboard, build pipeline, Postgres, Redis, registry, ingress, automatic SSL, observability, all of it — the only interesting question is the quantitative one: how many tenants does that box actually hold?

The answer up front: 10–25 tenants​

Here is the whole post in one paragraph of arithmetic. The server has 8 GB of RAM. The Kubernetes system layer takes roughly 1–1.5 GB, and the entire platform — every service named above — fits in about 750 MB.

That leaves around 5 GB for tenant workloads, or 4–5 GB once you stop pretending the platform idles forever. A small tenant (app plus Postgres) uses 200–300 MB of guaranteed memory; a medium tenant uses 400–500 MB. Divide headroom by footprint and you get 15–20 small tenants, 10–12 medium tenants, or 5–8 large ones — a realistic mixed fleet of 10–25 tenants on one $11/month ARM server.

That number comes from Jonathan Pitter's June 2026 teardown of Staxa, the multi-tenant deployment platform he runs on a single Hetzner CAX21. What makes the piece worth studying is not the headline — single-server heroics are a crowded genre — but the published usage tables underneath it. Every number below is his measured data, and the capacity math that falls out of it is the honest way to size the first node of any self-hosted PaaS.

The box: what €10.99 buys​

The server is a Hetzner CAX21, one of Hetzner's Ampere Altra ARM64 cloud instances: 4 shared vCPUs, 8 GB of RAM, and 80 GB of NVMe SSD for €10.99 a month (about $11 at the time of writing — Hetzner adjusted cloud pricing in June 2026, so confirm the current figure before budgeting). For context, Pitter notes the closest AWS equivalents are a t4g.large (2 vCPU, 8 GB ARM) at roughly $49 a month on-demand, or a t4g.xlarge (4 vCPU, 16 GB) at roughly $98 — a 4.5–9x price gap for comparable compute.

Why ARM? Because everything in the stack — Go, K3s, Traefik, PostgreSQL, Redis, Node.js, Buildah — ships native ARM64 support, and container tooling handles linux/arm64 images transparently. Pitter reports no performance penalty for his workload.

The real cost of ARM was unfamiliarity: most tutorials and Stack Overflow answers assume x86, which cost him a few hours of debugging early on and nothing since. That is the shape of the ARM tradeoff for containerized tenants in general — the price-per-container wins, and the tax is paid once, in porting assumptions, not continuously in performance.

Where the 8 GB goes​

Pitter runs K3s, so the memory layout splits into system components (outside the platform namespace) and platform services (inside it). The system side:

ComponentWhat it doesRAM
k3s serverAPI server, scheduler, controller manager, SQLite-backed store~500 MB
CoreDNSIn-cluster service DNS~50 MB
cert-managerAutomatic Let's Encrypt provisioning~50 MB
local-path-provisionerPersistentVolumes on the NVMe SSD~30 MB
Miscellaneouskubelet overhead, pause containers, system daemons~200 MB

Total system footprint: ~1–1.5 GB. K3s earns its place here: a full kubeadm-plus-etcd control plane would take 2–3 GB on its own, while K3s stays under 600 MB by replacing etcd with SQLite, stripping unused cloud-provider code, and shipping as a single ~70 MB binary. On a constrained box, that ~1.5 GB savings is the difference between running the platform and not running it.

The platform services are the more remarkable table, because these are not estimates — they are kubectl top pods from the live server:

ComponentWhat it doesRAM
Go API (staxad)All 60 API endpoints, K8s orchestration, job management13 Mi
Redis 7Deployment job queue, SSE pub/sub10 Mi
NotifierRedis-to-dashboard Socket.IO bridge (Node.js)20 Mi
Container registryStores tenant images for Buildah/Kubernetes33 Mi
PostgreSQL 16Platform database: tenants, deployments, configs44 Mi
TraefikIngress for all tenant HTTP/HTTPS traffic65 Mi
Next.js dashboardVisual tenant management85 Mi
GrafanaDashboards: logs, requests, errors, tenants98 Mi
LokiLog aggregation, backed by object storage154 Mi
Strapi CMSAPI docs and support-center content200 Mi

Total platform footprint: ~750 Mi, plus another ~30–50 Mi for Promtail in the observability namespace. API, database, queue, dashboard, CMS, registry, ingress, observability, and real-time event streaming fit in under 800 MB.

The standout is the Go API at 13 MB — less than the registry, less than Postgres, a fraction of any single JavaScript service in the stack.

Pitter's point generalizes: a Node.js or Java equivalent would idle at 100–200 MB, and at this scale the compiled-language advantage is not theoretical. It is the reason the platform fits on the server at all. If you are designing a control plane for a small first node, the implementation language of your API is a capacity decision, not a taste decision.

One caveat, which Pitter states plainly and any honest capacity plan must carry forward: these are baseline numbers from a platform in public testing with low traffic. Concurrent builds, API load, and more tenants all push consumption higher. Treat every table here as the floor, not the average.

The tenant math​

Each tenant gets a Kubernetes namespace containing an app container and a database container (PostgreSQL 16 or MySQL 8, tenant's choice), with quotas enforced per namespace. The offered tiers separate guaranteed requests from opportunistic burst:

SizeCPU requestRAM requestCPU burstRAM burstTypical use
Small25m64 MB500m512 MBStatic sites, light frontends
Medium50m128 MB1 CPU1 GBAPIs with database queries
Large100m256 MB2 CPU2 GBHeavy backends, workers

Requests are guaranteed minimums; burst is available when neighbors are quiet. On a lightly loaded server a small tenant can burst to half a CPU and 512 MB — plenty for a real web application. This requests-versus-burst split is doing quiet heavy lifting in the capacity story: guarantees are what you divide headroom by, but burst is what tenants actually feel, and generous burst on an uncongested box is why small tenants never notice they share a server.

Measured against the ~4–5 GB of realistic tenant headroom:

  • ~15–20 small tenants (200–300 MB guaranteed each)
  • ~10–12 medium tenants (400–500 MB guaranteed each)
  • ~5–8 large tenants for heavier workloads

In practice the fleet is a mix, hence the 10–25 tenant headline. Note what the math implies about RAM as the binding constraint: the box has 4 vCPUs and 8 GB, and tenants exhaust memory long before they exhaust CPU — at 25 tenants the guaranteed CPU sums to barely 1–2 cores. For the small-app workload profile, the first node of a self-hosted PaaS is a RAM-sizing exercise with CPU along for the ride.

The $13/month bill versus $127 on AWS​

The full monthly bill is almost comically short:

Line itemMonthly cost
Hetzner CAX21 server€10.99 (~$11)
Cloudflare DNS (wildcard + custom domains via CNAME)$0 (free tier)
SSL certificates (Let's Encrypt via cert-manager)$0
Container registry$0 (self-hosted)
Load balancer (Traefik, self-hosted)$0
Managed database (Postgres in containers)$0
Email (Resend free tier, up to 3k/month)$0
Domain (amortized)~$1.50
Total~$13/month

Against that, Pitter prices a minimal equivalent AWS stack: EKS control plane ($73) + t4g.medium worker ($25) + RDS micro ($12) + ALB ($16) + Route53 ($0.50) = ~$127/month, before bandwidth, storage IOPS, or a second availability zone. Roughly a 10x gap for day-one infrastructure.

The deepest cut in that comparison is the database line. A single AWS RDS db.t4g.micro costs about $12 a month — give every tenant their own and 10 tenants cost $120 in databases alone, more than 10x the entire Hetzner server.

Running each tenant's database as a container in its namespace, backed by a PersistentVolume on local NVMe, avoids a $12/tenant/month floor cost from day one. Is it as resilient as RDS with automated backups, failover, and point-in-time recovery? No, and Pitter does not pretend otherwise. But it keeps the architecture open: tenant database configuration is just a field in the API, so managed databases can arrive later as a premium tier without redesigning the platform. The lesson is about sequencing — buy resilience with revenue, not with runway.

Where the ceiling is​

A usage table is only as honest as its limits section, and this teardown has a good one. Three ceilings, in escalating order of when they bite:

Build contention arrives first. Container builds via Buildah are CPU-intensive, and on 4 shared vCPUs Pitter can realistically run 1–2 concurrent builds before performance degrades. Simultaneous tenant deploys queue. This is the shared-CPU ceiling in its purest form: steady-state serving is fine on shared cores, but bursty batch work (builds) contends immediately. Any capacity plan needs a build-concurrency policy, not just a tenant count.

Disk pressure arrives around 20–30 tenants. Tenant images accumulate in the registry, layers stay cached for rebuild speed, and the OS, Kubernetes, and databases all take their share of 80 GB. Registry garbage collection helps, but disk — not RAM, not CPU — is the bottleneck Pitter names as most likely to bite first in practice. Watch your registry growth curve; it is the early-warning metric.

The single point of failure is architectural. One server means platform and tenants go down together, with no redundancy. Acceptable for early access and demo environments; disqualifying for paying customers with uptime SLAs. The honest framing: this box buys you the runway to earn customers who justify the second box.

And the overarching caveat from earlier bears repeating: every number in this post is a low-traffic baseline. Loaded numbers will be higher. Size from the baseline, then leave the headroom — that is what the 4–5 GB planning figure (not 5 GB) is for.

The scaling ladder: €11 → €33 → ~€200​

K3s makes the growth story concrete, because joining a worker node is close to a single command and Kubernetes starts scheduling tenant pods across both servers automatically. Pitter's priced ladder:

SetupMonthly costTenant capacity
1x CAX21 (current)~€10.9910–25 tenants
CAX21 + CAX31 worker~€3330–60 tenants
CAX21 + AX42 dedicated worker~€20080–150 tenants
3-node CAX21 cluster~€3340–60 tenants with HA

Two things stand out. First, the jump from €10.99 to ~€33 roughly triples capacity — linear scaling at flat unit economics, which is exactly what you want a scaling ladder to show. Second, the ~€200 step pairs the cloud control plane with an AX42 dedicated worker (Ryzen, 64 GB-class RAM, local NVMe), buying an estimated 80–150 tenants: 10–15x the headroom for ~18x the spend, with dedicated cores replacing shared vCPUs precisely where the build-contention ceiling used to be. Compare adding an AWS worker node at $50–100 a month, and the self-hosted ladder keeps its order-of-magnitude lead at every rung.

The 3-node CAX21 row deserves a read of its own: same ~€33 as the two-node asymmetric setup, but spent on high availability (40–60 tenants with HA) instead of raw capacity (30–60 without it). That is the clearest illustration in the whole piece of what redundancy costs when infrastructure is cheap — you do not pay extra for HA, you just allocate the same €33 differently.

Pitter's hindsight notes round out the planning advice: he would start even smaller (the 2-vCPU CAX11 at ~€6.49 can technically run K3s plus the platform), and he would split control plane and tenant workloads earlier — a cheap CAX11 control node plus a CAX21 worker for ~€18 total, so a runaway tenant build can never peg the API and dashboard. Both suggestions point the same direction: isolate the control plane from tenant blast radius as soon as the second server is affordable, which on this ladder is almost immediately.

What this means for your first node​

Step back from Staxa specifically and the teardown teaches a capacity-planning method, not just a number:

  1. Publish the usage table, not the price. "Starts at $11/month" tells an operator nothing; a per-component RAM table plus per-tier tenant footprints tells them everything. If you run a self-hosted platform, your kubectl top output is the most useful documentation you can share.
  2. Size the first node on RAM. For small-tenant workload profiles, memory binds first — 8 GB holds 10–25 tenants while 4 vCPUs loaf. Buy the RAM headroom; burst covers the CPU.
  3. Name the first bottleneck explicitly. Here it is build concurrency (1–2 simultaneous), then disk at 20–30 tenants. Your platform has equivalents — find them before your tenants do.
  4. Sequence resilience after revenue. In-container databases and a single point of failure are the right day-one tradeoffs when the alternative is a $12/tenant floor cost; the architecture just has to admit the upgrade path later.

The headline number — a full PaaS for 10–25 tenants on $11 a month — is real, measured, and reproducible. But the durable artifact is the method: measure the baseline, divide headroom by footprint, price the ladder, and write down where the ceiling is. Do that and your first node is a plan, not a hope.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex