Vireo
A lightweight process manager & resource guardian for Node, Python and Docker.
Memory leaks, crash loops, runaway processes and restart storms waste developer machines and take down services. Existing process managers restart things after they die; Vireo watches resource usage continuously and acts before the crash — while staying tiny itself (a single ~8MB static binary, ~10MB RSS, <1% CPU).
What it does
- Supervises anything — Node scripts, Python scripts, arbitrary commands, and Docker containers, under one tool with one config file.
- Memory ceilings — soft limits with graceful restart on breach; OS-enforced hard limits where supported (Job Objects on Windows, cgroups via Docker).
- Leak detection — Theil-Sen trend analysis over each app's memory history detects sustained growth (robust against GC sawtooth and spikes) and restarts the app gracefully before it OOMs. Warmup grace periods prevent false positives on JIT/cache-heavy startups.
- Crash-loop breaking — restarts use decorrelated-jitter exponential backoff, and 5 crashes within 60 seconds halts the app instead of burning CPU forever.
- Correct process-tree management — on Windows, Job Objects guarantee that killing
npm run devalso kills thenodeit spawned (no orphans in Task Manager); on unix, process groups do the same. - Graceful shutdown everywhere — SIGTERM→SIGKILL on unix, CTRL_BREAK→terminate on Windows,
docker stopsemantics for containers, all with configurable grace windows. - Doesn't become the problem — fixed-size metric ring buffers, adaptive sampling cadence, a plain ndjson IPC protocol, zero heavy dependencies. Check it yourself:
vireo status --self.
Why Vireo over PM2?
| Vireo | PM2 | |
|---|---|---|
| Footprint | Single ~8MB static binary, ~10MB RSS, no runtime needed | Node.js daemon — requires a Node runtime and tens of MB of RSS |
| Memory handling | Detects sustained leaks (Theil-Sen trend) and restarts before OOM; OS-enforced hard caps | max_memory_restart reacts only after a threshold is crossed |
| What it supervises | Node, Python, arbitrary commands and Docker containers in one config | Processes only — no container supervision |
| Windows | First-class: Job Objects tree-kill, hard memory caps, graceful CTRL_BREAK | Known orphan-process and signal-handling gaps |
Install
npm
npm install -g @astrion-labs/vireo
pip
pip install vireo-pm
Docker
docker run -v ./vireo.yml:/etc/vireo/vireo.yml ghcr.io/astrion-labs/vireo
Binary — grab a release from GitHub Releases.
All three installs give you the same vireo command.
Quick start
Supervise something in the foreground (great for CI and containers):
vireo run --max-memory 512MB --restart on-failure -- node server.js
vireo run --leak-slope 5MB/min -- python worker.py
Or hand apps to the daemon so they outlive your shell:
vireo start --name api --max-memory 512MB -- node server.js
vireo list # live table: state, uptime, restarts, mem, cpu, trend
vireo logs api -f # follow logs
vireo status api # detail incl. memory trend + recent events
vireo status --self # the daemon's own footprint
vireo stop api
Ecosystem file
Define everything in a vireo.yml and start it with one command:
apps:
- name: api
cmd: node server.js
max_memory: 512MB
max_cpu: 150%
restart: on-failure
leak:
slope: 5MB/min # sustained growth beyond this is a leak
action: restart # restart gracefully before OOM
- name: worker
cmd: python worker.py
env:
QUEUE_URL: "${QUEUE_URL:-redis://localhost:6379}"
restart: always
- name: cache
kind: docker
image: redis:7
ports: ["6379:6379"]
mem_limit: 256MB # enforced by the kernel via cgroups
vireo start # detached under the daemon
vireo up # or foreground (containers / CI)
How leak detection works
Every 5 seconds Vireo samples the RSS of each app's entire process tree. Over a rolling window (default 10 minutes) it computes the Theil-Sen estimator — the median of all pairwise slopes — which ignores GC sawtooth patterns and one-off spikes that fool least-squares fits. When the trend exceeds your threshold (slope: 5MB/min) continuously for the sustain period (default 3 min), Vireo warns; if the projected time-to-OOM gets close and action: restart is set, it restarts the app gracefully at a moment of its choosing rather than letting the OS kill it at the worst one. A warmup period (default 2 min) after every start prevents false positives from JIT compilation and cache filling.
Command reference
| Command | Description |
|---|---|
vireo run -- <cmd> |
Supervise in the foreground, no daemon |
vireo up [file] |
Supervise all apps from a config file, foreground |
vireo start [file | -- <cmd>] |
Start app(s) under the daemon, detached |
vireo stop/restart/delete <app> |
Lifecycle (restart also re-arms halted apps) |
vireo list |
Table of all apps with live resources (--json for machines) |
vireo status <app> |
Detail: limits, trend, recent events |
vireo status --self |
The daemon's own resource usage |
vireo logs <app> [-f] [-n N] |
Recent logs / live follow |
vireo kill-daemon |
Stop the daemon and all apps |
Key flags for run/start: --name, --max-memory 512MB, --max-cpu 150, --hard-limit, --restart always|on-failure|never, --stop-grace 10s, --leak-slope 5MB/min, --leak-action warn|restart, --leak-window, --leak-sustain, --leak-warmup, --cwd.
Docker integration
Vireo talks to the Docker Engine API directly (no docker CLI needed). Containers declared with kind: docker get: pull-if-missing, create, start, kernel-enforced memory limits, restart-on-exit with the same backoff/breaker logic as processes, restart-on-unhealthy (healthcheck integration), and the same leak detection driven by container stats. Set adopt: true to guard a container that already exists by name. If the Docker engine isn't running, everything else keeps working.
To guard sibling containers from inside a container, mount the socket:
docker run -v /var/run/docker.sock:/var/run/docker.sock \
-v ./vireo.yml:/etc/vireo/vireo.yml ghcr.io/astrion-labs/vireo
Design notes
- Single static Go binary, CGO-free, cross-compiled to Windows/Linux/macOS on amd64+arm64. The npm and pip packages just ship this binary (the esbuild/ruff model) — no runtime dependency on Node or Python beyond the shim.
- Daemon on demand:
vireo start/list/...auto-spawn a per-user daemon speaking ndjson over a unix socket (unix) or named pipe (Windows, ACL'd to your user).vireo run/vireo upnever spawn one. - State survives restarts: the daemon persists desired state and relaunches your apps if it's restarted.
- Windows is first-class: Job Objects for tree-kill and hard memory caps, CTRL_BREAK_EVENT for graceful shutdown (handle
SIGBREAKin Node,signal.SIGBREAKin Python), named-pipe IPC.
Building from source
go build -ldflags "-s -w" -o vireo .
go test ./...
License
Apache-2.0 Astrion Labs