Skip to content
Infrastructure

Sizing the Server

A sizing model for my self-hosted stack: a reservation and a limit for every service, from Traefik and Jenkins to each API and web app per environment, a formula with headroom, the numbers worked through on a 4 vCPU, 16 GB server, and the signals that say it is time to grow.

Before an application goes on the server, it should be clear that it fits. This is the model I size the server with: one figure per service for what it needs, a formula that adds headroom, and the signals that say the model has stopped being true.

The figures are starting estimates for a small production load, a few requests a second on each API, not measurements. After a week of real traffic, Grafana’s numbers replace them.

Two figures per service

  • Reservation: what the service needs at steady state to stay healthy.
  • Limit: the ceiling it may burst to. Services rarely burst together, so limits can add up to more than the server has; reservations must not.

The platform

ServiceCPURAMDisk
Traefik0.25 vCPU128 MB—
Docker socket proxies, three0.05 vCPU48 MB—
Status API0.10 vCPU128 MB—
Console0.05 vCPU32 MB—
Prometheus, 30 days0.25 vCPU768 MB10 GB
Loki, 31 days0.25 vCPU384 MB15 GB
Alloy0.10 vCPU128 MB—
Grafana0.10 vCPU192 MB1 GB
Jenkins controller0.25 vCPU1024 MB5 GB
Jenkins agent, idle0.10 vCPU256 MB—
PostgreSQL, on the host0.50 vCPU1536 MB40 GB
Platform total2.00 vCPU4624 MB71 GB

Without PostgreSQL and Jenkins’ controller, the platform comes to about 2 GB, which matches yggdrasil’s own guidance for a host: 2 GB for the platform, 4 GB with the Jenkins controller, plus the applications.

The applications

Each system is an API and a web app, and runs once per environment that is on:

ServiceCPURAM
ASP.NET Core API0.50 vCPU384 MB
Flutter web app, on nginx0.10 vCPU48 MB
One system in one environment0.60 vCPU432 MB

With Heimdall and Fortuna, each environment that is on adds 1.2 vCPU and 864 MB.

The formula

Every dimension is sized the same way:

text
Required = OS baseline + sum of reservations + headroom
Headroom = at least 30% of (OS baseline + sum of reservations)

The OS and Docker take about 1 vCPU, 1 GB of RAM and 20 GB of disk. Docker images take disk of their own: three kept per application and environment for rollbacks, plus the build cache of the .NET and Flutter SDKs, about 20 GB in all.

Worked through on the server

Two cases matter: production alone, which is most of the time, and everything on at once, while development and homologation are in use.

DimensionProduction only+30%All three environments+30%Server
CPU4.2 vCPU5.5 vCPU6.6 vCPU8.6 vCPU4 vCPU
RAM6.4 GB8.3 GB8.0 GB10.5 GB16 GB
Disk111 GB144 GB111 GB144 GB200 GB

RAM and disk fit with room to spare, even with every environment on: more than 5 GB of RAM stays free beyond the headroom, for PostgreSQL’s cache and for bursts, and about 55 GB of disk for growth.

CPU is the tight dimension, and it is the reason two design choices exist:

  • On-demand environments. Development and homologation idle at almost no CPU when stopped, which they are most of the time. The reservations assume load, and only production has load.
  • Builds are bursts. Building an image on the host, especially a Flutter web build, takes several cores for a few minutes. It runs at the moment of a deploy, next to production, so deploys at quiet hours cost nothing.

CPU reservations are a planning figure, not a hard split: an idle service reserves 0.5 vCPU on paper and uses almost none. The model says the server is fully booked on CPU under real load, which is the signal to watch first.

Enforcing limits

Compose can cap each container, so one runaway service cannot starve the rest:

yaml
services:
  api:
    cpus: "1.50"
    mem_limit: 768m
    mem_reservation: 384m

PostgreSQL runs on the host, so its limits are its own settings instead: shared_buffers at about a quarter of its memory, and effective_cache_size at what the operating system can cache for it.

When to grow

Signal, sustainedAction
Host CPU above 80% for 15 minutesAdd vCPU, or move PostgreSQL to its own server
Free RAM below 2 GB, or swappingAdd RAM, or shorten Prometheus’s and Loki’s retention
Disk above 80%Grow the disk, shorten retention, or keep fewer rollback images
API latency at the 95th percentile climbingAdd a second API replica behind Traefik
Builds slowing production downBuild in GitHub Actions and push to a registry, so the server only pulls
Development or homologation always onGive them a server of their own, with its own Jenkins agent

The order that keeps the most for the least:

  1. Shorten observability retention: the cheapest win.
  2. Add an API replica: APIs are stateless, so it is one more container.
  3. Move development and homologation off the box: yggdrasil already supports one agent per host, so it is a catalog change and a second server.
  4. Move PostgreSQL to its own server: the biggest single reclaim of CPU and memory.