Skip to content
BoringStack
GitHub

Resource limits

4 min read

Container Sizing

Every container in the stack has deploy.resources.limits and deploy.resources.reservations set from env vars. The defaults target a 4-vCPU / 8 GB VPS, the cheapest tier on Hetzner / OVH / DigitalOcean that’s usable in production. Bigger box? Bump the knobs.

4 vCPU

default target

8 GB

base RAM

Env-driven

all knobs

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 + 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 (~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.

Default resource allocations for a 4 vCPU / 8 GB host
Service
CPU limit
RAM limit
Notes
Postgres
1.0
512 MB
Heaviest of the data plane; bump first.
Valkey
0.5
256 MB
Plenty for cache + queues at small scale.
Traefik
0.5
256 MB
Steady-state usage is low.
API (dev/prod)
1.5
1 GB
Generous for Bun + Drizzle; covers burst on cold caches.
UI (dev)
1.5
1 GB
Vite dev server uses memory during HMR.
UI (prod, nginx)
0.25
128 MB
Static file serving is cheap.

Sum of limits exceeds host capacity on purpose: containers don’t all peak simultaneously. Reservations are sized so nothing can starve.

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.

Diagnose OOM kill
$ docker inspect <container> --format '{{.State.OOMKilled}}'
$ docker inspect <container> --format '{{.State.ExitCode}}'
$ docker stats

!   true  ← OOMKilled=true means memory limit was hit
!   137   ← exit code 137 = SIGKILL by OOM
#   NAME        CPU %   MEM USAGE / LIMIT

If OOMKilled=true, bump the matching *_LIMITS_MEMORY knob in .env and redeploy. If it keeps happening, the underlying code is leaking; fix the leak, don’t keep raising the ceiling.

docker stats shows live usage so you can see how close services run to their limits in steady state. Anything pegged >80% of its memory limit is a candidate for a bump.

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