Skip to content

Resources & scaling

cgroup limits, scale-to-zero, replicas, blue/green deploys, and CPU autoscaling.

Updated View as Markdown

Everything here is optional — an app runs as a single always-on instance with no limits by default.

Resource limits

homeport.yamlyaml
resources:
  memory: 512M   # hard cap; throttled at 90%, OOM-killed at 100%
  cpu: 150%      # 150% = 1.5 cores

These become systemd cgroup directives (MemoryMax / MemoryHigh / CPUQuota) — the same kernel mechanism Docker uses for --memory / --cpus. Omit the block for no limits. homeport stats shows the cap next to live usage.

Scale to zero

homeport.yamlyaml
idle: true
idle_timeout: 5m   # default 5m

For low-traffic apps. systemd holds the app’s port and starts the binary on the first request, then stops it after idle_timeout of no traffic — zero RAM while asleep, so a box can hold far more mostly-idle apps. Implemented with systemd socket activation + systemd-socket-proxyd, so it works for any binary.

The first request after idle pays the cold-start (fast for Go, ~1s for a big JS-framework binary).

Replicas & rolling deploys

homeport.yamlyaml
replicas: 3

Runs N instances (systemd template units), load-balanced by Caddy (least_conn

  • passive health checks that pull a dead replica out). Deploys become rolling and zero-downtime: instances restart one at a time, health-checked, while the others keep serving.

Works for public and internal apps alike — a public app is balanced behind its domain; an internal app is balanced on loopback, so callers keep using 127.0.0.1:<port> but get HA and more cores. Size replicas to the box’s cores; they mainly help single-process runtimes (Node/Bun) use more cores. Mutually exclusive with idle.

Zero-downtime activation (blue/green)

A single-instance public app deploys blue/green by default — no config. The new release starts on a private port while the old one keeps serving; once it passes its health check, Caddy flips to it and the old instance retires. Traffic never drops; an unhealthy release never gets the flip (verified: 0/400 requests dropped across a live flip).

Two instances coexist for a few seconds during each deploy — fine for stateless web apps, but a singleton (holds an exclusive lock, or a scheduler that must never double-run) can’t tolerate it:

homeport.yamlyaml
strategy: recreate   # restart in place (brief downtime, never 2 at once)

Multi-instance apps already deploy rolling; internal and path-mounted single-instance apps restart in place (give them replicas: 2 for zero-downtime); idle apps use socket activation.

Autoscaling

homeport.yamlyaml
autoscale:
  min: 1
  max: 4
  target_cpu: 70   # scale up above, down below; default 70

A systemd timer samples each replica’s CPU every 20s and adjusts the running count between min and max, with hysteresis and a 60s cooldown so it can’t flap. Scales on CPU, not memory — replicas add cores, so CPU is the signal scaling actually relieves. Most useful for single-process runtimes (Node/Bun): idle runs min replicas, load bursts to max. Ceiling is the box’s cores. Mutually exclusive with idle and fixed replicas.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close