Resource limits
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
Why limits matter
Section titled “Why limits matter”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.
Design choices
Section titled “Design choices”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.
The shape of a knob
Section titled “The shape of a knob”Each service has four env vars: limits CPU/memory, reservations CPU/memory. Example for Postgres:
POSTGRES_LIMITS_CPUS=1.0POSTGRES_LIMITS_MEMORY=512MPOSTGRES_RESERVATIONS_CPUS=0.25POSTGRES_RESERVATIONS_MEMORY=128MSame pattern for VALKEY_, TRAEFIK_, API_DEV_, UI_DEV_, API_, UI_, and the optional overlays.
Default sizing (4 vCPU / 8 GB)
Section titled “Default sizing (4 vCPU / 8 GB)”Sum of limits exceeds host capacity on purpose: containers don’t all peak simultaneously. Reservations are sized so nothing can starve.
Sizing for bigger hosts
Section titled “Sizing for bigger hosts”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.
Diagnosing an OOM kill
Section titled “Diagnosing an 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 / LIMITIf 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.
Source
Section titled “Source”compose/.env.example; every knob, commented. compose/docker-compose.yml; where they’re wired into deploy.resources. docs/resource-limits.md; extended sizing guide.
Related
Section titled “Related”- Profiles & overlays; overlays add their own services with their own limits.
- Observability; Grafana dashboards show resource pressure over time.