Skip to content
BoringStack
Star

Resource limits

3 min read

Every container has deploy.resources.limits (hard cap) and deploy.resources.reservations (guaranteed minimum) set from env vars. The defaults target a 4-vCPU / 8 GB VPS, the cheapest production-viable tier on Hetzner / OVH / DigitalOcean. Bigger host? Bump the knobs in compose/.env.

A misbehaving worker consuming all memory shouldn’t take Postgres down with it. Limits draw the boundaries; reservations guarantee a service can boot even when the host is busy. Without limits, one runaway process kills every other service on the host.

  • Limits plus reservations on every service. Failures stay isolated; the host stays responsive.
  • Knobs in compose/.env, not hardcoded. Right-sizing is a config change, not a code change.
  • Defaults target 4 vCPU / 8 GB. Cheapest production-viable VPS tier; everything else scales up from there.
  • Reservations less than limits (about 1:4 ratio). Reserves the minimum to boot; allows bursts up to the limit.
  • Bun and Node services share roughly the same shape. Avoids per-runtime tuning until measurements say otherwise.

Each service has four env vars: limits CPU/memory, reservations CPU/memory. Example for Postgres:

Terminal window
POSTGRES_LIMITS_CPUS=1.0
POSTGRES_LIMITS_MEMORY=512M
POSTGRES_RESERVATIONS_CPUS=0.25
POSTGRES_RESERVATIONS_MEMORY=128M

Same pattern for VALKEY_, TRAEFIK_, API_DEV_, UI_DEV_, API_, UI_, and the optional overlays.

ServiceCPU limitMemory limitNotes
Postgres1.0512 MBHeaviest of the data plane; bump first when OOMing.
Valkey0.5256 MBSufficient for cache + queues at small scale.
Traefik0.5256 MBReverse proxy; low steady-state usage.
API (dev/prod)1.51 GBBun + Drizzle; generous to cover cold-cache bursts.
UI (dev)1.51 GBVite dev server memory during HMR.
UI (prod)0.25128 MBStatic file serving is cheap.

Limits sum to more than the host on purpose: containers don’t peak simultaneously. Reservations are small so the host stays responsive under load.

Each service has four env vars in compose/.env:

Terminal window
POSTGRES_LIMITS_CPUS=1.0
POSTGRES_LIMITS_MEMORY=512M
POSTGRES_RESERVATIONS_CPUS=0.25
POSTGRES_RESERVATIONS_MEMORY=128M

Same pattern for VALKEY_, TRAEFIK_, API_DEV_, UI_DEV_, API_, UI_, and optional overlays.

  • 4 vCPU / 8 GB host. Postgres at default. One of each worker / API replica.
  • 8 vCPU / 16 GB host. POSTGRES_LIMITS_MEMORY=2G, POSTGRES_LIMITS_CPUS=2.0. Consider horizontal API replication.
  • 16+ vCPU / 32+ GB host. Postgres deserves its own host. Multiple API + worker replicas; revisit Valkey Cluster.

Above the 16 vCPU mark, the single-host model itself becomes the bottleneck; that’s the right time to look at the planned Kubernetes path.

Check if a container was killed for exceeding memory:

Terminal window
docker inspect <container-name> --format '{{.State.OOMKilled}}'
docker inspect <container-name> --format '{{.State.ExitCode}}'

OOMKilled=true means the memory limit was exceeded. Exit code 137 is SIGKILL by OOM.

Terminal window
docker stats

Shows live CPU and memory usage. Watch for services pegged >80% of their limit; they are candidates for a bump.

If a service OOMs repeatedly after bumping the limit, the code is likely leaking memory. Fix the leak rather than keep raising the ceiling.

compose/.env.example; every knob, commented. compose/docker-compose.yml; where they’re wired into deploy.resources. docs/resource-limits.md; extended sizing guide.