A small, fast process manager for the machines you actually ssh into.
Important
pilotlight is pre-release. No version has been tagged yet, so the install commands
below will not resolve until v0.1.0 ships.
Describe your processes in one TOML file. pilotlight start hands them to a small
supervisor daemon that keeps them alive, captures their output, and stays running after
you close the terminal. One static binary, no Node runtime, no config-in-JavaScript.
$ pilotlight start
✔ migrate exited 0 (312ms)
✔ api 4 instances pid 4412 4413 4414 4415
✔ worker 2 instances pid 4416 4417
✔ caddy 1 instance pid 4418
$ exit # your processes do not careHomebrew
brew install cmoresid/tap/pilotlightcurl
curl -fsSL https://pilotlight.sh/install.sh | shThe script detects your platform, downloads the matching release tarball from GitHub,
verifies its SHA-256 checksum, and installs to ~/.local/bin. It never invokes sudo on
your behalf. Read it first if you would rather not pipe to a shell —
site/install.sh.
Set PILOTLIGHT_VERSION to pin a version, or PILOTLIGHT_INSTALL_DIR to install elsewhere:
curl -fsSL https://pilotlight.sh/install.sh | PILOTLIGHT_INSTALL_DIR=/usr/local/bin shCargo
cargo install pilotlightpilotlight init # writes a starter pilotlight.toml
$EDITOR pilotlight.toml
pilotlight start
pilotlight status
pilotlight logs api -finit drops a minimal config in the current directory:
[[process]]
name = "web"
command = "npm run start"
[process.env]
PORT = "3000"- One file describes the whole stack. Committed to your repo, reviewed like code, the same on your laptop and on the box.
- It outlives your shell. A daemon owns the processes, so closing your terminal — or losing your ssh connection — changes nothing.
- Instances are a number, not a copy-paste.
instances = 4runs four copies with distinct ports via{instance}templating. - Restarts have a policy. Exponential backoff, a restart budget inside a time window,
and a process that is parked as
erroredinstead of crash-looping forever. - Logs are captured, not lost. Per-process files on disk plus a prefixed, colorized
logs -f. - Scriptable. Every command takes
--json. - One binary. Nothing to install first — no runtime, no package manager, no daemon written in a language you do not ship.
pilotlight resolves its config from, in order: the -c/--config flag, $PILOTLIGHT_CONFIG,
then pilotlight.toml in the current directory — walking up parent directories until it
finds one.
A complete example lives at examples/pilotlight.toml:
[pilotlight]
log_dir = "~/.pilotlight/logs"
# Inherited by every [[process]]; any process can override any of these.
[defaults]
restart = "on-failure"
restart_delay = "1s"
restart_delay_max = "30s"
restart_window = "60s"
max_restarts = 10
stop_signal = "SIGTERM"
stop_timeout = "10s"
# A one-shot task. pilotlight waits for a clean exit before starting
# anything that depends_on it.
[[process]]
name = "migrate"
command = "./bin/migrate up"
cwd = "./server"
oneshot = true
restart = "never"
[[process]]
name = "api"
command = "node dist/server.js"
cwd = "./server"
instances = 4
depends_on = ["migrate"]
env_file = [".env", ".env.production"]
[process.env]
NODE_ENV = "production"
PORT = "300{instance}" # api.0 -> 3000, api.1 -> 3001, ...Every key below is valid inside [[process]], and all except name and command are also
valid inside [defaults].
| Key | Type | Default | Description |
|---|---|---|---|
name |
string | required | Unique identifier. Instances are addressed as name.0, name.1, … |
command |
string | required | The command line. Split with shell-style quoting; run directly, not through a shell. |
cwd |
path | config's directory | Working directory. Relative paths resolve against the config file. |
instances |
integer | 1 |
How many copies to run. |
restart |
"always" | "on-failure" | "never" |
"on-failure" |
When to bring a dead process back. |
restart_delay |
duration | "1s" |
Delay before the first restart; doubles each retry. |
restart_delay_max |
duration | "30s" |
Ceiling for the backoff. |
restart_window |
duration | "60s" |
Restarts are counted within this rolling window. |
max_restarts |
integer | 10 |
Restarts allowed per window before the process is parked as errored. |
stop_signal |
string | "SIGTERM" |
Signal sent on stop. |
stop_timeout |
duration | "10s" |
Grace period before SIGKILL. |
oneshot |
boolean | false |
Run to completion instead of supervising. Dependents wait for a clean exit. |
depends_on |
array of names | [] |
Start ordering. Cycles are rejected by config check. |
env_file |
string or array | [] |
.env files to load, later files winning. Relative to cwd. |
env |
table | {} |
Environment variables. Values may use {instance}. |
Durations accept ms, s, m, h — "500ms", "90s", "5m".
Inside [process.env] values, {instance} expands to the zero-based instance index. It is
the intended way to give each copy its own port:
instances = 4
[process.env]
PORT = "300{instance}" # 3000, 3001, 3002, 3003Every child also receives these, whether or not you use templating:
| Variable | Value |
|---|---|
PILOTLIGHT_PROCESS_NAME |
The process name, e.g. api |
PILOTLIGHT_INSTANCE_ID |
Zero-based index, e.g. 2 |
PILOTLIGHT_INSTANCES |
Total instance count, e.g. 4 |
| Command | What it does |
|---|---|
pilotlight init |
Write a starter pilotlight.toml into the current directory |
pilotlight start [NAME...] |
Start everything, or only the named processes |
pilotlight stop [NAME...] |
Send stop_signal, then SIGKILL after stop_timeout |
pilotlight restart [NAME...] |
Stop, then start |
pilotlight reload [NAME...] |
Rolling restart — one instance at a time, so the others keep serving |
pilotlight status |
Table of every process: name, id, pid, status, restarts, cpu, memory, uptime |
pilotlight logs [NAME] [-f] [-n N] |
Tail captured output, prefixed and colorized per process |
pilotlight scale NAME=N |
Change instance count without editing the config |
pilotlight config check |
Validate the config and print it fully resolved |
pilotlight daemon start|stop|status |
Manage the supervisor daemon directly |
pilotlight startup |
Print — or with --install, install — a launchd or systemd unit for boot start |
Global flags: -c, --config <PATH>, --json, --no-color, -v, --verbose.
$ pilotlight status
NAME ID PID STATUS ↺ CPU MEM UPTIME
migrate - - exited 0 - - -
api 0 4412 running 0 0.4% 48.2 MB 2h 14m
api 1 4413 running 0 0.3% 47.9 MB 2h 14m
api 2 4414 running 1 0.4% 48.1 MB 6m 02s
api 3 4415 running 0 0.3% 47.4 MB 2h 14m
worker 0 4416 running 0 1.1% 61.0 MB 2h 14m
worker 1 4417 errored 10 - - -
caddy 0 4418 running 0 0.1% 12.3 MB 2h 14mpilotlight the CLI is a thin client. The processes are owned by a supervisor daemon that
the first start spawns automatically and that stays running until you stop it. They talk
over a unix domain socket.
~/.pilotlight/daemon.sock control socket (override: $PILOTLIGHT_SOCKET)
~/.pilotlight/state.json survives daemon restarts
~/.pilotlight/logs/api-0.out.log captured stdout
~/.pilotlight/logs/api-0.err.log captured stderr
Because the daemon — not your shell — is the parent, processes survive terminal exit and
dropped ssh sessions. pilotlight daemon stop shuts the daemon down and stops everything
it owns.
stdout and stderr are captured to separate files per instance under log_dir. logs
merges them back together, newest last:
$ pilotlight logs -f
api.0 | GET /healthz 200 1.2ms
worker.1 | job 8812 complete
caddy.0 | serving on :443Pass a name to filter (pilotlight logs api), -n to change how many lines of history to
replay, and -f to follow.
pilotlight startup # prints the unit file for your platform
pilotlight startup --install # writes it and enables itmacOS gets a launchd agent in ~/Library/LaunchAgents; Linux gets a systemd user unit
in ~/.config/systemd/user. Neither requires root. On Linux you will want
loginctl enable-linger $USER so the unit starts without an active login session.
Requires Rust 1.85 or newer (2024 edition).
git clone https://github.com/cmoresid/pilotlight
cd pilotlight
cargo build --release
./target/release/pilotlight --version-
v0.1.0— config parsing, daemon,start/stop/restart/status/logs -
reload(rolling restart) andscale -
startupfor launchd and systemd - HTTP and command health checks, with restart-on-unhealthy
- Log rotation and retention
- Resource limits (
memory_limit, restart-on-exceed) - Windows support
Issues and pull requests are welcome. cargo build and cargo test must pass; CI runs both
on every push.
MIT — see LICENSE.