A language-agnostic process manager (PM2-like) with built-in log rotation
pm3 start ./worker.sh --name worker
pm3 start ./worker.py --interpreter python3 --name py-worker --cwd /srv/app
pm3 list
pm3 logs worker
pm3 stop worker# npm, pnpm, yarn or bun. The launcher downloads the native binary for this
# platform on first run; nothing is downloaded at install time.
npm install -g @tomasko/pm3
# From the MoonBit package registry, if the MoonBit toolchain is already
# installed. Builds from source and installs `pm3` to ~/.moon/bin.
moon install kupas/pm3/cmd/pm3
# Install script, no Node.js required.
curl -LsSf https://raw.githubusercontent.com/tomasky/pm3/master/install.sh | bash
# Or build from source, see below.| Platform | Release asset |
|---|---|
| Linux x86-64 | pm3-linux-x86_64 (static) |
| Linux arm64 | pm3-linux-aarch64 (static) |
| macOS Apple silicon | pm3-darwin-aarch64 |
$ pnpm add @tomasko/pm3
Error: ERR_PNPM_IGNORED_BUILDS
× adding a new package
╰─▶ Ignored build scripts: @tomasko/pm3@0.1.1moon build --target native --release
# the binary is at _build/native/release/build/cmd/pm3/pm3.exemoon build --target native
./_build/native/debug/build/cmd/pm3/pm3.exe listsudo apt-get install musl-tools
./scripts/build-linux-static.sh dist/pm3-linux-x86_64moon test --target nativegit tag v0.1.1 && git push origin v0.1.1# Start a service. The name defaults to the script's basename ("worker").
pm3 start ./worker.sh
# Run a non-executable script through an interpreter.
pm3 start ./jobs/digest.py --interpreter python3 --name digest
# The extension already picks the interpreter (.js -> node); arguments for
# the interpreter itself go before the script, as PM2 places them.
pm3 start ./app.js --interpreter-args "--trace-warnings --max-old-space-size=512"
# Pass arguments to the managed process after `--`.
pm3 start ./server --name api -- --port 8080
# Environment variables.
pm3 start ./server --name api -e PORT=8080 -e LOG_LEVEL=info
# Inspect it. The columns are PM2's — id, name, namespace, version, mode, pid,
# uptime, restarts (↺), status, cpu, mem, user, watching — and the footer
# totals the running processes.
pm3 list
# ┌────┬────────┬───────────┬─────────┬──────┬──────┬────────┬───┬────────┬───────┬────────┬──────┬──────────┐
# │ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
# ├────┼────────┼───────────┼─────────┼──────┼──────┼────────┼───┼────────┼───────┼────────┼──────┼──────────┤
# │ 0 │ digest │ default │ N/A │ fork │ 4242 │ 6s │ 0 │ online │ 86.9% │ 11.3mb │ root │ disabled │
# │ 1 │ worker │ default │ 2.5.1 │ fork │ 4243 │ 6s │ 0 │ online │ 0% │ 2.0mb │ root │ disabled │
# └────┴────────┴───────────┴─────────┴──────┴──────┴────────┴───┴────────┴───────┴────────┴──────┴──────────┘
#
# 2/2 online total cpu 86.9% total mem 13.3mb
pm3 describe api
# Describing process with id 0 - name digest
# ┌────────────────────┬───────────────────────────────────┐
# │ pm_id │ 0 │
# │ name │ digest │
# │ status │ online │
# │ pid │ 21557 │
# │ command │ python3 ./digest.py │
# │ cwd │ <inherited> │
# │ out log │ /root/.pm3/logs/digest-out.log │
# │ memory │ 11.3mb │
# │ cpu │ 86.9% │
# └────────────────────┴───────────────────────────────────┘
pm3 logs digest
# Stop it, remove it.
pm3 stop api
pm3 delete apipm3 start # reads ./pm3.toml
pm3 config apps # show what that would start| What it describes | Where pm3 looks | |
|---|---|---|
| Applications | Which processes to run, and how | $PM3_CONFIG, then ./pm3.toml, ./pm3.json, then the global file |
| Log rotation | The rotation policy, shared by every process | the global file only |
# pm3.toml
[defaults] # optional, applied to every app below
max_restarts = 16
max_memory_restart = "200M"
[[app]]
name = "worker"
script = "./worker.py"
interpreter = "python3"
args = ["--verbose"]
env = { PORT = "8080", LOG_LEVEL = "info" }
[[app]]
name = "api"
script = "./bin/server"
max_memory_restart = "1G" # overrides the default for this app only
stop_exit_codes = [0, 2]{
"defaults": { "max_restarts": 16 },
"app": [
{ "name": "worker", "script": "./worker.py", "interpreter": "python3" }
]
}| Key | Type |
|---|---|
| name | string (default: the script's basename without its extension) |
| script | string, required |
| namespace | string (default default); pm3 stop\|restart\|delete <namespace> acts on all of its apps |
| args | array of strings |
| interpreter | string (exec_interpreter is accepted as PM2's alias) |
| interpreter_args | string (split on whitespace) or array; PM2's node_args and interpreterArgs are accepted too |
| cwd | string (default: the config file's directory) |
| env | table of scalars |
| autorestart | bool |
| max_restarts, restart_delay, min_uptime, kill_timeout | integer |
| max_memory_restart | size ("200M", "1G", or bytes) |
| kill_signal | string |
| stop_exit_codes | array of integers |
| merge_logs | bool |
| out_log, err_log | string |
| log_date_format | string |
pm3 start # ./pm3.toml, or the global file
pm3 start --config prod.toml
pm3 start pm3.json # a positional .toml/.json is a config file
pm3 config apps # show what that would start, and validate the filepm3 start <script> ... is unchanged: a single script is still started directly, and options after the script are passed to the program, so pm3 start sh -c 'echo hi' works. Use --config when the ambiguity matters.
$ pm3 start ./worker.rb
Interpreter ruby is NOT AVAILABLE in PATH. (type 'which ruby' to double check.)pm3 start ./app.js --interpreter-args "--trace-warnings --max-old-space-size=512"interpreter_args = "--trace-warnings --max-old-space-size=512" # split like a shell
interpreter_args = ["--trace-warnings", "--max-old-space-size=512"]| Command | Description |
|---|---|
| pm3 start <script> [options] [-- args...] | Start and supervise a process |
| pm3 start [--config <file>] | Start every app a config file declares |
| pm3 list (aliases ls, ps, status) | Table of managed processes with PM2's columns, plus running/total counts and total CPU and memory |
| pm3 describe <name\|id> (aliases desc, info, show) | Full configuration and runtime state |
| pm3 stop [name\|id\|namespace\|all] | Stop one process, a whole namespace, or all when all (or no name) is given |
| pm3 restart [name\|id\|namespace\|all] | Restart one process, a whole namespace, or all (resets the restart counter) |
| pm3 delete [name\|id\|namespace\|all] (alias del) | Stop and remove processes; a namespace name removes all of its apps |
| pm3 logs [name\|id\|namespace] [--lines N] [--nostream] [--timestamp [fmt]] [--out] [--err] [--raw] | Tail the logs, then stream new lines (PM2's behaviour); --nostream keeps the tail alone |
| pm3 flush [name] | Truncate the live log(s) |
| pm3 reloadLogs | Rotate every managed log now |
| pm3 logrotate | Show the rotation policy |
| pm3 set logrotate:<key> <value> | Change the rotation policy |
| pm3 config | Show the active config file and its contents |
| pm3 config init [toml\|json] [--force] | Create a config file |
| pm3 config apps [--config <file>] [--json] | Show what pm3 start would start, without starting it |
| pm3 save | Persist the process list to dump.json |
| pm3 resurrect | Start the processes that were desired online |
| pm3 update | Restart the daemon with this build, then resurrect what was online |
| pm3 kill | Stop the daemon (and its processes) |
| pm3 version | Print the version |
| Option | Meaning |
|---|---|
| --config <file> | Read the app list from a pm3.toml / pm3.json instead |
| -n, --name <name> | Process name (default: script basename) |
| --namespace <ns> | Start the app within the given namespace (default default) |
| -i, --interpreter <cmd> | Run <script> as an argument to <cmd> |
| --interpreter-args <arguments> | Arguments for the interpreter, before the script (--node-args is PM2's alias) |
| --cwd <dir> | Working directory |
| --merge-logs | Send stderr into the stdout log |
| --out-file <path> | stdout log path |
| --err-file <path> | stderr log path |
| --no-autorestart | Do not restart the process when it exits |
| --max-restarts <n> | Restart budget (0 = unlimited) |
| --restart-delay <ms> | Delay before each restart |
| --min-uptime <ms> | Runs shorter than this count as "unstable" |
| --max-memory-restart <size> | Restart when RSS exceeds <size> |
| --kill-timeout <ms> | Grace period before SIGKILL |
| --kill-signal <signal> | Graceful signal (default SIGINT) |
| --stop-exit-codes <a,b> | Exit codes that should not trigger a restart |
| --time | Prefix every log line with PM2's default YYYY-MM-DDTHH:mm:ss timestamp |
| --log-date-format <fmt> | Custom moment-style timestamp prefix for log lines (wins over --time) |
| -e, --env KEY=VALUE | Set an environment variable (repeatable) |
pm3 <command> pm3 daemon
┌────────────────────┐ ┌──────────────────────────┐
│ client package │ loopback │ daemon package │
│ parse CLI │ ─────────► │ accept + dispatch │
│ connect to daemon │ newline- │ supervisor (per app) │
│ print results │ delimited │ log sinks + rotation │
└────────────────────┘ JSON │ gzip + prune workers │
│ resource monitor │
└────────────┬─────────────┘
│ pipes
┌─────────┴──────────┐
│ managed children │
└────────────────────┘~/.pm3/
├── dump.json # serialized process table
├── config.toml # configuration (or config.json)
├── daemon.port # ephemeral TCP port (while running)
├── daemon.pid # daemon pid (while running)
├── pm3.log # daemon's own stdout/stderr
└── logs/
├── <name>-out.log
├── <name>-err.log
└── <name>-out__<stamp>.log[.gz]pm3 logrotatepm3 set logrotate:max_size 100M
pm3 set logrotate:compress true
pm3 set logrotate:retain 14
pm3 set logrotate:rotate_interval "0 3 * * *"| Key | Default | Meaning |
|---|---|---|
| max_size | 10M | Rotate at this size (0 disables size rotation) |
| min_size | 0 | Never rotate below this size |
| retain | 30 | Number of rotated files to keep per log |
| max_age_days | 0 | Also delete rotated files older than this (0 = off) |
| compress | false | Gzip rotated files in the background |
| compression_level | 9 | gzip level, 1–9 (validated and stored; the built-in encoder writes at a fixed level) |
| date_format | YYYY-MM-DD_HH-mm-ss | Timestamp tokens used in rotated names |
| worker_interval | 30 | Seconds between size/prune passes (min 1) |
| rotate_interval | 0 0 * * * | Cron expression that forces a rotation |
| tz | system | Timezone for file names (UTC+8, +05:30, or an IANA name) |
| rotate_module | true | Reserved; kept for pm2-logrotate compatibility |
# ~/.pm3/config.toml
[logrotate]
max_size = "100M"
retain = 14
compress = true
rotate_interval = "0 3 * * *"
tz = "UTC+8"{
"logrotate": {
"max_size": "100M",
"retain": 14,
"compress": true
}
}| Variable | Meaning |
|---|---|
| PM3_HOME | State directory (default $HOME/.pm3) |
| PM3_LOG_DATE_FORMAT | Date format for pm3.log line prefixes (default YYYY-MM-DDTHH:mm:ss; empty disables the prefix) |
| PM2_LOG_DATE_FORMAT | Fallback for PM3_LOG_DATE_FORMAT, so a PM2 environment keeps its format |
| PM2_KILL_SIGNAL | Default graceful-stop signal (default SIGINT), PM2's variable |
| PM3_DAEMON_TITLE | ps title for the daemon (default PM3 v<version>: God Daemon (<PM3_HOME>)) |
| PM2_DAEMON_TITLE | Fallback for PM3_DAEMON_TITLE, PM2's variable |
pub(all) struct App {
pm_id : Int
name : String
app_namespace : String
script : String
args : Array[String]
interpreter : String
interpreter_args : Array[String]
cwd : String
env : Map[String, String]
out_log : String
err_log : String
merge_logs : Bool
log_date_format : String
autorestart : Bool
max_restarts : Int
restart_delay : Int
min_uptime : Int
max_memory_restart : Int64
kill_timeout : Int
kill_signal : String
stop_exit_codes : Array[Int]
status : String
version : String
username : String
desired_state : String
pid : Int
restarts : Int
unstable_restarts : Int
created_at : Int64
started_at : Int64
stopped_at : Int64
exit_code : Int
out_log_size : Int64
err_log_size : Int64
memory : Int64
cpu : Double
last_start_ms : Int64
} derive(Eq, ToJson, Debug, FromJson)fn LogrotateConfig::set_field(self : LogrotateConfig, key : String, value : String) -> Result[Unit, String]fn bg_black_bright(text : String) -> Stringfn blue_bold(text : String) -> Stringfn bold_cyan(text : String) -> Stringfn bold_gray(text : String) -> Stringfn bold_inverse(text : String) -> Stringfn civil_from_days(days : Int) -> (Int, Int, Int)let cmd_start_apps : Stringlet cmd_version : Stringfn colorized_metric(value : Double, warn : Double, alert : Double, text : String) -> Stringfn colors_enabled() -> Boolfn current_username() -> Stringfn daemon_process_title() -> Stringfn days_from_civil(year : Int, month : Int, day : Int) -> Intfn default_kill_signal() -> Stringlet default_kill_timeout_ms : Intfn default_log_path(name : String, stream : String) -> Stringfn derive_config_app_name(script : String) -> Stringfn find_app_config() -> String?fn find_package_version(script : String, base : String) -> Stringfn format_cpu_percent(cpu : Double) -> Stringfn format_duration(millis : Int64) -> Stringfn format_fixed(value : Double, precision : Int) -> Stringfn format_size(bytes : Int64, precision? : Int) -> Stringfn format_size_exact(bytes : Int64) -> Stringfn format_size_pm2(bytes : Int64, precision? : Int) -> Stringfn format_timestamp(epoch_secs : Int64, offset_secs : Int, fmt : String) -> Stringfn format_uptime_pm2(millis : Int64) -> Stringfn green_bold(text : String) -> Stringfn interpreter_for_extension(extension : String) -> String?fn is_app_config_file(path : String) -> Boolfn is_binary_content(bytes : Bytes) -> Boolfn is_daemon_log_path(path : String) -> Boolfn is_python_interpreter(interpreter : String) -> Boolfn pad_left_text(text : String, width : Int, pad : Char) -> Stringfn pad_right(text : String, width : Int, pad : Char) -> Stringfn pad_visible(text : String, width : Int, pad : Char) -> Stringfn parse_bool_value(text : StringView) -> Bool?fn parse_non_negative_int(text : StringView) -> Int?fn parse_rotated_stamp(log_path : String, file_name : String) -> String?fn parse_size(text : StringView) -> Int64?fn parse_size_or(text : StringView, default : Int64) -> Int64fn path_dirname(path : String) -> Stringfn path_join(left : String, right : String) -> Stringfn pm3_daemon_log_path() -> Stringfn pm3_home() -> Stringfn pm3_logrotate_config_path() -> Stringfn pm3_pid_path() -> Stringfn pm3_port_path() -> Stringfn pm3_state_dir() -> Stringlet pm3_version : Stringfn red_bold(text : String) -> String┌────┬────────┐
│ id │ name │
├────┼────────┤
│ 0 │ worker │
└────┴────────┘fn resolve_start_path(script : String, cwd : String?, base : String) -> (String, String?)fn rotated_path(log_path : String, stamp : String, compress : Bool) -> Stringfn script_extension(path : String) -> Stringfn select_for_deletion(files : Array[RotatedFile], retain : Int, max_age_days : Int, now : Int64) -> Array[String]fn set_colors_enabled(enabled : Bool) -> Unitfn split_timestamp(local_secs : Int64) -> (Int, Int, Int, Int, Int, Int, Int)fn strip_suffix_owned(name : String, suffix : String) -> Stringfn style_blue(text : String) -> Stringfn style_gray(text : String) -> Stringfn style_green(text : String) -> Stringfn style_inverse(text : String) -> Stringfn style_red(text : String) -> Stringfn style_yellow(text : String) -> Stringfn truncate_text(text : String, width : Int) -> Stringfn visible_width(text : String) -> Intfn weekday_from_days(days : Int) -> Intfn yellow_bold(text : String) -> StringInstall
Download zipA language-agnostic process manager (PM2-like) with built-in log rotation