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 --follow
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
# 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/main/main.exemoon build --target native
./_build/native/debug/build/cmd/main/main.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
# 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 footer totals the running processes.
pm3 list
# id name status cpu mem restarts uptime command
# -- ----- ------ ----- ------ -------- ------ ------------------
# 0 worker online 0.1% 24 MB 0 2h14m ./worker.sh
# 1 digest online 91.5% 118 MB 3 41m python3 ./digest.py
#
# 2/2 online total cpu 91.6% total mem 142.13 MB
pm3 describe api
pm3 logs api --follow
# Stop it, remove it.
pm3 stop api
pm3 delete apipm3 start # reads ./pm3.toml
pm3 config apps # what would that 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 |
| args | array of strings |
| interpreter | string |
| 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 # check what would be startedpm3 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.
| 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 running/total counts and total CPU and memory |
| pm3 describe <name> | Full configuration and runtime state |
| pm3 stop [name] | Stop one process, or all when no name is given |
| pm3 restart [name] | Restart one process, or all (resets the restart counter) |
| pm3 delete [name] (alias del) | Stop and remove processes |
| pm3 logs [name] [--lines N] [--follow] [--err] | Show (and follow) logs |
| 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 | List the apps the config file declares |
| pm3 save | Persist the process list to dump.json |
| pm3 resurrect | Start the processes that were desired 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) |
| -i, --interpreter <cmd> | Run <script> as an argument to <cmd> |
| --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 SIGTERM) |
| --stop-exit-codes <a,b> | Exit codes that should not trigger a restart |
| -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 | 6 | gzip level, 1–9 (validated; encoder level is fixed) |
| 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
compression_level = 6
rotate_interval = "0 3 * * *"
tz = "UTC+8"{
"logrotate": {
"max_size": "100M",
"retain": 14,
"compress": true
}
}| Variable | Meaning |
|---|---|
| PM3_HOME | State directory (default $HOME/.pm3) |
pub(all) struct App {
pm_id : Int
name : String
script : String
args : Array[String]
interpreter : 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
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 civil_from_days(days : Int) -> (Int, Int, Int)let cmd_start_apps : Stringfn days_from_civil(year : Int, month : Int, day : Int) -> Intfn default_log_path(name : String, stream : String) -> Stringfn derive_config_app_name(script : String) -> Stringfn find_app_config() -> String?fn 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_timestamp(epoch_secs : Int64, offset_secs : Int, fmt : String) -> Stringfn is_app_config_file(path : String) -> Boolfn pad_left_text(text : String, width : Int, pad : Char) -> Stringfn pad_right(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() -> Stringfn rotated_path(log_path : String, stamp : String, compress : Bool) -> Stringfn select_for_deletion(files : Array[RotatedFile], retain : Int, max_age_days : Int, now : Int64) -> Array[String]fn split_timestamp(local_secs : Int64) -> (Int, Int, Int, Int, Int, Int, Int)fn strip_suffix_owned(name : String, suffix : String) -> Stringfn truncate_text(text : String, width : Int) -> Stringfn weekday_from_days(days : Int) -> IntInstall
Download zipA language-agnostic process manager (PM2-like) with built-in log rotation