procman
A foreman-like process supervisor written in Rust. Reads a YAML config file, spawns all listed jobs, multiplexes their output with name prefixes, and tears everything down cleanly when any child exits or a signal arrives. See the full documentation for detailed guides on configuration, dependencies, templates, and more.
Usage
cargo install --path .
The first positional argument is the path to the config file (required). Arguments after -- are parsed according to config.args definitions (see below).
Dependency graph
Most service ordering is handled declaratively in the config file. Jobs with no depends list start immediately; jobs with dependencies are held until every condition is met. This forms a DAG — circular dependencies are detected at parse time.
jobs:
migrate:
run: db-migrate up
once: true
web:
run: serve --port 3000
api:
depends:
- process_exited: migrate
- url: http://localhost:3000/health
code: 200
timeout_seconds: 30
run: api-server start
Here migrate and web start immediately. api waits for migrate to exit successfully and for web to pass its health check — no scripting required. Available dependency types include HTTP health checks, TCP connect, file exists, file contains, process exited, and their negations. See the Config Format section below and the Dependencies chapter for the complete reference.
-e / --env — inject environment variables
A repeatable -e KEY=VALUE flag for ad-hoc environment variable injection without modifying the config file. Precedence (lowest → highest): system env → CLI -e → YAML env: block.
--debug — pause before shutdown
When a child process fails, procman pauses before sending SIGTERM, prints which process triggered the shutdown and which processes are still running, and waits for ENTER or Ctrl+C to proceed. Requires an interactive terminal.
Config Format
config:
logs: ./my-logs # optional: custom log directory (default: procman-logs)
args: # optional: user-defined CLI arguments (parsed after --)
rust_log:
short: r
description: "RUST_LOG configuration"
type: string
default: "info"
env: RUST_LOG
enable_feature:
type: bool
default: false
env: FEATURE_ENABLED
jobs:
web:
env:
PORT: "3000"
run: serve --port $PORT
migrate:
run: db-migrate up
once: true
api:
depends:
- process_exited: migrate
- url: http://localhost:3000/health
code: 200
poll_interval: 0.5
timeout_seconds: 30
run: api-server start
setup:
depends:
- path: /tmp/ready.flag
run: post-setup-task
db:
depends:
- tcp: "127.0.0.1:5432"
run: db-client start
healthcheck:
depends:
- not_listening: "127.0.0.1:8080"
- not_exists: /tmp/api.lock
- not_running: "old-api.*"
run: api-server --port 8080
optional-worker:
condition: test -n "$WORKER_ENABLED"
run: worker-service start
nodes:
for_each:
glob: "/etc/nodes/*.yaml"
as: NODE_CONFIG
run: node-agent --config $NODE_CONFIG
once: true
recovery:
run: ./scripts/recover.sh
autostart: false
web-watched:
run: web-server --port 8080
watch:
- name: health
check:
url: http://localhost:8080/health
code: 200
initial_delay: 5.0
poll_interval: 10.0
failure_threshold: 3
on_fail: shutdown
- name: disk
check:
path: /var/run/healthy
on_fail:
spawn: recovery
The config file has two top-level keys:
config(optional): global settings.logs(optional): custom log directory path (default:procman-logs).args(optional): map of user-defined CLI arguments parsed from argv after--. Each key is the arg name (underscores in YAML become dashes on the CLI, e.g.rust_log→--rust-log). Fields:type(optional, defaultstring):stringorbool. String args take a value (--name VALUE), bool args are flags (--name= true).short(optional): single-character or short string for-sshorthand.description(optional): help text shown with-- --help.default(optional): fallback value. Args without a default are required.env(optional): environment variable name to inject the arg value into (e.g.env: RUST_LOG).
- Arg values are available as
${{ args.var_name }}templates inrunandenvfields. - Env precedence (lowest → highest): system env → arg defaults → CLI
--args →-eflags → YAMLenv:blocks.
jobs(required): map of job name to job definition.
Each job definition supports:
-
run(required): the command to execute. All commands are passed tosh -euo pipefail -c, so shell features (pipes, redirects,&&, variable expansion) work in both single-line and multi-line commands. The strict flags mean unset variable references and pipeline failures are treated as errors. Supports${{ process.KEY }}templates to reference output values fromoncedependencies, and${{ args.name }}to reference user-defined arg values. -
env(optional): per-process environment variables (also supports${{ }}templates). -
once(optional): iftrue, the process exits cleanly on success (code 0) without triggering supervisor shutdown. Processes can write key-value pairs to$PROCMAN_OUTPUTfor downstream template resolution. -
for_each(optional): fan-out a template process across glob matches. Requiresglob(pattern) andas(variable name). Each match spawns an instance with the variable set in env and substituted in the run string. -
depends(optional): list of dependencies that must be satisfied before the process starts. Circular dependencies are detected at config parse time. Dependency fields (url,tcp,path,file_contains.path,not_listening,not_exists,not_running) support$VARand${VAR}environment variable expansion (including per-processenvoverrides); use$$for a literal$.- HTTP health check:
url+code(expected status), with optionalpoll_intervalandtimeout_seconds. - TCP connect:
tcp(address:port), with optionalpoll_intervalandtimeout_seconds. - File exists:
pathto a file that must exist. - File contains key:
file_containswithpath,format(json/yaml),key(JSONPath expression per RFC 9535, e.g.$.database.url), and optionalenv(variable name to extract the value into). With optionalpoll_intervalandtimeout_seconds. - Process exited:
process_exitednames aonce: trueprocess that must complete successfully before this process starts. Supports an expanded form for timeout control:process_exited: { name: ..., timeout_seconds: 30 }. Usetimeout_seconds: nullto wait indefinitely (the simple string form defaults to 60 seconds). - TCP not listening:
not_listening(address:port), with optionalpoll_intervalandtimeout_seconds. Waits until no service is accepting connections. - File not exists:
not_existspath that must not exist. - Process not running:
not_runningpattern (matched viapgrep -f). Waits until no matching process is found.
All dependency types accept an optional
retry(defaulttrue). Setretry: falseto fail immediately if the dependency is not satisfied on the first check — useful to catch stale state (leftover lock files, ports still bound, zombie processes). - HTTP health check:
-
condition(optional): a shell command evaluated before spawning. If it exits non-zero, the job is skipped entirely. Runs viash -euo pipefail -cin the process's resolved environment. Skippedonce: truejobs are registered as exited soprocess_exiteddependents can proceed. Supports${{ args.name }}templates. -
autostart(optional, defaulttrue): iffalse, the process is dormant — it won't start until explicitly spawned via a watchon_fail: spawnaction. -
watch(optional): list of runtime health checks that monitor the process after it starts. Each watch polls a check (same types as dependencies) and takes an action when consecutive failures exceed the threshold.name(optional): human-readable name; auto-generated if omitted.check(required): same syntax as a dependency (HTTP, TCP, file exists, etc.).initial_delay(optional, default 0): seconds to wait before the first check.poll_interval(optional, default 5): seconds between checks.failure_threshold(optional, default 3): consecutive failures before triggering the action.on_fail(optional, defaultshutdown): action to take —shutdown,debug,log, orspawn: <process_name>(starts a dormant process withPROCMAN_WATCH_*env vars).
Behavior
- Each child runs in its own process group; shutdown signals reach all descendants.
- stderr is merged into stdout per-process.
- Output is prefixed with the process name, right-aligned and padded.
- Per-process logs are written to
<log_dir>/<name>.log(directory is recreated each run; default./procman-logs/). - A combined
<log_dir>/procman.logcontains the full interleaved formatted output (same as stdout). - On SIGINT or SIGTERM, all children receive SIGTERM. After a 2-second grace period, remaining processes are sent SIGKILL.
- procman exits with the first child's exit code.
License
MIT