parax
v3.0.0
Published
Run and keep alive multiple processes in parallel from a JSON descriptor
Maintainers
Readme
parax
parallel exec
parax runs multiple commands in parallel from a JSON descriptor file. Each command's output goes to its own log file, while parax shows a status dashboard. It supports restarting crashed processes and a built-in web server that tracks restart counts.
Installation
npm install --global paraxRequires Node.js 18.20 or newer on Linux or macOS. Windows is not supported: parax relies on POSIX process groups.
Usage
parax commands.jsonDescriptor
The descriptor is validated before anything starts. Unknown keys, wrong types and out-of-range values are rejected with a one-line error such as parax: commands.json: unknown key "keepalive" in descriptor, and parax exits with code 1. Strings must not contain null bytes.
commands (required)
Array of command objects to run in parallel.
| Field | Type | Required | Description |
| ------ | -------- | -------- | -------------------------------------------------------------------- |
| cmd | string | yes | The executable to run (not empty, not run through a shell) |
| log | string | yes | Log file for the process's stdout and stderr (appended, see below) |
| name | string | no | Name shown in the status output (default: cmd and args joined) |
| args | string[] | no | Arguments to pass to the executable (default: []) |
| cwd | string | no | Working directory for the process |
| env | object | no | Environment variables for the process (replaces parent env) |
Log files
Each process's stdout and stderr are appended to its log file, which is created with mode 0640 (before umask) if missing. Relative paths are resolved against parax's working directory, not the command's cwd. The file is reopened on every restart, so rotating it by renaming takes effect on the next restart; copytruncate works at any time. For security, parax refuses to write through a symlink at the log path (ELOOP). A log file that cannot be opened counts as a failed start. Processes get no stdin.
Status output
When stdout is a terminal, parax shows a dashboard that is redrawn whenever a process starts or exits:
COMMAND STATUS RESTARTS STARTED LAST EXIT LOG
api running 0 2025-01-15 09:30:00 - /var/log/api.log
worker restarting 7 2025-01-15 09:32:10 code 1 /var/log/worker.log
└ Error: listen EADDRINUSE: address already in use :::3000After a failed run, the └ line shows the last line that run wrote to its log, so startup crashes are visible without opening the log files. Terminal escape sequences are removed from it.
When stdout is not a terminal (systemd, docker, redirected to a file), parax prints the table at startup and every 5 minutes, plus one timestamped line whenever a process exits:
2025-01-15 09:32:10 worker exited with code 1. Restarting... └ Error: listen EADDRINUSE: address already in use :::3000Descriptor options
keepAlive
Boolean. When true, any process that exits is automatically restarted. Defaults to false.
loopMillis
Integer from 0 to 2147483647. Delay (in milliseconds) before restarting an exited process when keepAlive is true. Defaults to 2000.
webPort
Integer from 1 to 65535. When set, starts an HTTP server on 127.0.0.1 at the given port exposing restart counts at GET /restarts. It is only reachable from the local machine.
Requests must use 127.0.0.1:<port> or localhost:<port> as the Host header; anything else gets 403. This blocks websites from reading the endpoint through DNS rebinding. The server accepts at most 32 connections and closes a connection that has not sent a complete request within 2 seconds.
Exit code
0when all commands exited with code 0, or when parax was stopped by a signal.1when any command exited with a non-zero code, was killed by a signal, or could not be started (withoutkeepAlive), or when the descriptor is invalid.
With keepAlive, parax only exits when stopped by a signal.
Signals
Each command runs in its own process group. When a command's main process exits, any processes it left behind (e.g. under sh -c or npm run) receive SIGTERM. With keepAlive, whatever is still left of that group receives SIGKILL right before the restart, so leftovers get loopMillis to shut down gracefully.
On SIGINT, SIGTERM or SIGHUP, parax stops restarting processes, forwards the signal to every command's process group, and exits once all main processes have exited. A second signal while stopping sends SIGKILL to every process group instead.
When running parax as PID 1 in a container, use docker run --init (or tini). Parax cannot reap orphaned grandchildren, so without an init process they remain as zombies.
Security
- The descriptor is code: whoever can edit it can run any command as the user running parax. Make sure only that user can write to it, and never run parax as root with a descriptor other users can modify.
- Commands run without a shell, so values in
cmd,args,cwdandenvare never interpreted as shell syntax. - Commands inherit parax's user and privileges. Run parax as an unprivileged user where possible.
- Log files are opened by parax with its privileges. Keep log directories writable only by trusted users; parax does not follow a symlink at the log path itself, but does follow symlinked directories.
/restartsshows full command lines, including any secrets passed as arguments. It is only reachable from the local machine (seewebPort).
Examples
Basic parallel execution:
{
"commands": [
{ "name": "frontend", "cmd": "npm", "args": ["run", "dev"], "cwd": "./frontend", "log": "frontend.log" },
{ "name": "backend", "cmd": "npm", "args": ["run", "start"], "cwd": "./backend", "log": "backend.log" }
]
}Keep services alive:
{
"keepAlive": true,
"commands": [
{ "cmd": "node", "args": ["db.js"], "log": "/var/log/db.log" },
{ "cmd": "node", "args": ["server.js"], "log": "/var/log/server.log" }
]
}Track restarts via the web server:
{
"keepAlive": true,
"webPort": 3000,
"commands": [
{ "cmd": "node", "args": ["worker.js"], "log": "worker.log" },
{ "cmd": "node", "args": ["api.js"], "log": "api.log" }
]
}GET http://localhost:3000/restarts
[
{ "cmd": "node worker.js", "restarts": 2, "uptimeMs": 12000 },
{ "cmd": "node api.js", "restarts": 0, "uptimeMs": 60000 }
]uptimeMs is the time in milliseconds since the current process instance started (reset on each restart). It is null while the process is not running.
Development
pnpm install
pnpm check # type-check source and tests
pnpm test # build, then run the test suiteThe test suite uses Node's built-in test runner and requires Node 22.18 or newer (TypeScript type stripping) on a POSIX system. Tests run the built dist/index.js as a black box. To run parax itself under a different Node.js version than the test runner, set PARAX_NODE to that node binary:
PARAX_NODE=/path/to/node-18/bin/node pnpm testnpm pack and npm publish run the type check and the full test suite first (prepack).
