Skip to content

cmoresid/pilotlight

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pilotlight

A small, fast process manager for the machines you actually ssh into.

Build License: MIT Website


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 care

Install

Homebrew

brew install cmoresid/tap/pilotlight

curl

curl -fsSL https://pilotlight.sh/install.sh | sh

The 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 sh

Cargo

cargo install pilotlight

Quick start

pilotlight init          # writes a starter pilotlight.toml
$EDITOR pilotlight.toml
pilotlight start
pilotlight status
pilotlight logs api -f

init drops a minimal config in the current directory:

[[process]]
name    = "web"
command = "npm run start"

[process.env]
PORT = "3000"

Why pilotlight

  • 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 = 4 runs 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 errored instead 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.

Configuration

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, ...

Process keys

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".

{instance} templating

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, 3003

Every 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

Commands

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 14m

How it works

pilotlight 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.

Logs

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 :443

Pass a name to filter (pilotlight logs api), -n to change how many lines of history to replay, and -f to follow.

Starting at boot

pilotlight startup            # prints the unit file for your platform
pilotlight startup --install  # writes it and enables it

macOS 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.

Building from source

Requires Rust 1.85 or newer (2024 edition).

git clone https://github.com/cmoresid/pilotlight
cd pilotlight
cargo build --release
./target/release/pilotlight --version

Roadmap

  • v0.1.0 — config parsing, daemon, start/stop/restart/status/logs
  • reload (rolling restart) and scale
  • startup for 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

Contributing

Issues and pull requests are welcome. cargo build and cargo test must pass; CI runs both on every push.

License

MIT — see LICENSE.

Releases

Packages

Contributors

Languages