docker-compose.yml ships recommended resource bounds so the app can’t
OOM or starve its neighbors — and neighbors can’t starve it.
Shipped defaults
| Service | CPU limit | Memory limit | Memory reservation |
|---|---|---|---|
| proxmoxvex | 2.0 | 2G | 512M |
| postgres | 4.0 | 2G | 256M |
- limits cap worst-case usage — a runaway query loop or a huge API response can’t take the whole host.
- reservations (k8s:
requests) reserve baseline capacity — a busy host won’t evict the app below its floor.
The compose file also sets shm_size: 256m on postgres: parallel
workers and parallel VACUUM allocate dynamic shared-memory (DSM)
segments under /dev/shm, and the docker default of 64MB fails with
could not resize shared memory segment on large tables. The 4.0 CPU
limit reflects co-hosted databases — the evlog app’s dashboard issues
~14 concurrent aggregate queries per page load, which serialized to
~2.8s of wall time on a single core. For an install where postgres
serves only this app, 1.0/1G remains adequate (that is still what
docker-compose.template.yml ships).
Right-sizing the app container
The 2G/512M default covers a typical install (a few clusters, UI + API traffic, background schedulers). Scale memory for:
- P2V/V2V conversion jobs — add ~512M per concurrent conversion. Conversions stream disk images through the app; two parallel jobs → ~3G limit.
- Large fleets — every connected cluster holds a live manager session and RRD caches; expect roughly +100M per ~5 additional clusters.
- Report/export bursts — the audit-integrity walk and large exports run inside request workers; keep ~256M headroom over observed p95 RSS.
Scale CPU for concurrent conversion/backup transfers and busy websocket consoles — 2.0 handles the UI easily; add cores for heavy parallel guest actions rather than for web traffic (the gevent server is IO-bound, not CPU-bound).
Kubernetes equivalent
resources:
requests: { memory: 512Mi, cpu: 250m }
limits: { memory: 2Gi, cpu: "2" }
Verifying limits under load
scripts/load-tests.sh runs the k6 suite against a running instance —
with limits applied (compose honors deploy.resources under the compose
spec), watch docker stats proxmoxvex during the run: steady-state RSS
should stay well under the limit and the container must not OOM-restart.
If it does, raise memory first — CPU pressure shows as latency, memory
pressure shows as restarts.
Read-only root filesystem (hardened deployments)
The compose template runs the app with read_only: true — a compromised
process cannot persist inside the image. Every write path is an explicit
mount, so verify all of them exist before removing mounts:
proxmoxvex_config:/app/config— settings, keys, db backupsproxmoxvex_logs:/app/logs— rotating log filesproxmoxvex_plugins:/app/plugins— plugin install/update state. A fresh volume starts empty and is seeded from the image’s/app/plugins-seed; on every later boot the shipped plugin dirs are reconciled with the image copy — new plugin dirs are copied in, changed files are synced byte-for-byte, and files the image no longer ships are pruned — while plugin-declaredmutable_pathsstate,__pycache__, andintegrity.jsonare preserved, and installed dirs reporting a newer manifest version are never downgraded (paid plugins arrive via license-server artifacts, so the seed never contains them). The catalog metadata filedirectory.jsonis likewise app-owned and refreshed from the seed on every boot so image upgrades can’t leave a stale catalog (retired paid plugins would otherwise keep resurfacing in/api/pluginsas undeletable “download” entries).tmpfs /tmpandtmpfs /run— scratch for tempfiles and the ssh_pool socket dir
At startup the app runs a writable-path check and logs [STARTUP] ... is not writable naming any missing mount — check docker logs if a
hardened deployment misbehaves. App updates on read-only deployments go
through image pulls (scripts/deploy-rolling.sh), not in-place writes.