Proctor
A Procfile-compatible process manager with hot reload, readiness probes, dependencies, and more
Install / Use
/learn @alecthomas/ProctorREADME
A Procfile-compatible process manager with hot reload, readiness probes, dependencies, and more
Overview
Proctor is a local development process manager that is compatible with and extends the Procfile format. It aims to replace ad-hoc combinations of foreman, watchexec, and shell scripts with a single declarative file.
Features:
- Procfile-compatible syntax with shell-style quoting and line continuation
- One-shot processes (run to completion) and long-running processes
- File watching with glob patterns and exclusions (
.gitignorerespected automatically) - Hot reload on file changes with configurable debounce and signal
- Process dependencies with
after=for ordered startup - Readiness probes (TCP port, HTTP endpoint, or shell command)
- Automatic restart on crash with exponential backoff
- Graceful shutdown in reverse dependency order
- Global and per-process environment variables
- Per-process working directory
- Coloured, aligned log output with optional timestamps
- Multiline command blocks
Example
# Long-running infrastructure with TCP readiness probe
postgres ready=5432: docker run --rm -p 5432:5432 postgres:16
# One-shot: run migrations before anything else (multiline command block)
migrate! **/*.sql dir=./db after=postgres:
echo "Running migrations..."
psql -f schema.sql
psql -f seeds.sql
# One-shot with file watching: re-run codegen when schema changes
codegen! **/*.graphql debounce=1s: npm run codegen
# Long-running with HTTP readiness probe, depends on postgres
api **/*.go !**_test.go after=migrate,postgres ready=http:8080/health
debounce=1s signal=INT shutdown=10s:
LOG_LEVEL=debug go run ./cmd/api
# Long-running in subdirectory, watches multiple file types and restarts if codegen is re-run
frontend web/**/*.{ts,tsx,css,html} !web/dist/** dir=./web after=api,codegen: \
NODE_ENV=development npm run dev
Installation
curl -fsSL https://raw.githubusercontent.com/alecthomas/proctor/master/install.sh | sh
To install a specific version or to a custom directory:
curl -fsSL https://raw.githubusercontent.com/alecthomas/proctor/master/install.sh | INSTALL_DIR=~/.local/bin sh -s v0.1.0
Environment Variables
| Variable | Description |
|-----------------|--------------------------------------------------|
| PROCTOR_FLAGS | Default flags (shell-quoted), e.g. -dt --check |
Procfile Syntax
Line format
<proc>[!] [!]<pattern>... [option=value ...]: [ENV=VALUE ...] <command>
Each line defines a process. The colon (:) separates the declaration (left) from the execution (right). Tokenisation uses shell-style rules throughout: bare words, 'single quoted' (literal), and "double quoted" (with escape sequences).
Comments and blank lines
Lines starting with # are comments. Blank lines are ignored.
Global environment variables
Lines matching KEY=VALUE (with no colon) define global environment variables that are set for all processes:
CGO_ENABLED=0
NODE_ENV=development
api: go run ./cmd/api
frontend: npm run dev
Values can be bare, single-quoted (literal), or double-quoted (with escape sequences like \n, \t):
SIMPLE=value
SPACES='hello world'
NEWLINE="line1\nline2"
Global variables are merged with the inherited environment. Inline ENV=value in the command takes precedence over global variables.
Line continuation
A trailing \ continues the command onto the next line, following shell conventions:
api **/*.go: go run \
-tags dev \
./cmd/api
Multiline command blocks
If the colon is immediately followed by a newline, subsequent indented lines form the command:
build!:
echo "Building..."
go build -o bin/app ./cmd/app
echo "Done"
Common leading indentation is stripped. The block ends at the first non-indented line (or end of file).
Declaration (left of :)
Process name
The first token is always the process name. It must be unique within the file. Valid characters: [a-zA-Z0-9_-].
A trailing ! marks the process as one-shot (expected to run to completion and exit). Without the !, processes are assumed to be long-running (expected to stay alive).
migrate!: just db migrate # one-shot: ready when it exits 0
api: go run ./cmd/api # long-running: ready immediately on start
Watch patterns
Any token after the process name that is not an option (key=value) is interpreted as a file watch pattern. Patterns follow standard globbing rules including ** for recursive matching and {a,b} for alternation. Bare file names are also supported.
A token prefixed with ! is an exclusion pattern.
api **/*.go !**_test.go !vendor/**:
echo Procfile: echo "Procfile changed"
If no watch patterns are present, the process is not file-watched.
Paths matching .gitignore rules (and the .git/ directory) are automatically excluded from file watching. This means you don't need to manually add exclusions for directories like node_modules/, target/, dist/, etc.
Options
Any token matching key=value that is not a glob and not the process name is an option. Values follow shell quoting rules.
| Option | Type | Default | Description |
|-------------|----------|-------------|--------------------------------------------------------|
| after | string | | Dependency — wait for named process to become ready |
| ready | string | | Readiness probe (see Readiness) |
| signal | string | TERM | Signal to send on reload (HUP, INT, TERM, etc.) |
| debounce | duration | 500ms | Debounce interval for file change events |
| dir | path | . | Working directory for the process |
| shutdown | duration | 5s | Grace period after signal before SIGKILL |
Multiple dependencies can be specified with comma separation: after=redis,migrate.
Note on one-shot processes: The ready option is not permitted for one-shot processes since they become ready when they exit 0. All other options are valid, including watch patterns (to re-run the one-shot when files change).
Execution (right of :)
Environment variables
Tokens matching KEY=VALUE before the command set environment variables for the process. These are merged on top of the inherited environment. Inline values take precedence.
Command
Everything after environment variables is the command, executed via the system shell ($SHELL or /bin/sh). This means pipes, redirects, and subshells work as expected:
api: go run ./cmd/api 2>&1 | grep -v healthcheck
Process Lifecycle
Classification
Processes are classified by the ! suffix on their name:
- One-shot (
name!): Expected to run to completion and exit. Becomes ready when it exits with code 0. - Long-running (
name): Expected to stay alive. Becomes ready immediately on start (unless areadyprobe is specified).
Startup order
- Parse the Procfile and build a dependency graph from
after=declarations. - Start all processes with no dependencies concurrently.
- As each process becomes ready (exits 0 for one-shot, passes readiness probe or starts for long-running), start any processes that were waiting on it.
If a one-shot process exits non-zero, startup is aborted and all running processes are shut down. Circular dependencies are detected at parse time and treated as an error.
Hot Reload
When a file change matches a process's glob patterns (after exclusions):
- Debounce: wait for the configured debounce interval with no further matching changes.
- For running processes: send the configured signal (default
SIGTERM) to the process group, wait up toshutdownduration, thenSIGKILLif needed. - For completed one-shot processes: start immediately.
- Restart the process.
- All downstream dependents (processes with
after=pointing to the reloaded process, transitively) are also restarted, in dependency order, after the upstream process becomes ready.
Restart on crash
If a long-running process exits unexpectedly (not due to a reload or shutdown):
- Restart immediately on first failure.
- Apply exponential backoff on consecutive failures: 1s, 2s, 4s, 8s, 16s, capped at 32s.
- Gradually reset the backoff level while running stably (decrease one level after running for the current backoff duration).
- Log each restart with the exit code or signal.
Shutdown
On SIGINT or SIGTERM to proctor itself:
- Send
SIGTERMto all process groups in reverse dependency order. - Wait up to each process's
shutdownduration. SIGKILLany remaining processes.- Exit.
Readiness
The ready option defines how proctor determines a process is ready (for after= dependents).
| Format | Behaviour |
|-----------------------------------|------------------------------------------------------------------------|
| <port> | Poll localhost:<port> until a TCP connection succeeds |
| http:<port>[/<path>][=<status>] | Poll http://localhost:<port>[/<path>] for the expected status code |
| exec:<command> | Run <command> via shell; ready when it exits 0 |
If no =<status> is specified for HTTP probes, any non-5xx response is accepted. If =<status> is specified, only that exact status code is accepted.
With no ready option: long-running processes are ready immediately on start; one-shot processes are ready on exit 0.
Readiness polling begins when the process starts, with a 250ms interval and a 30s timeout. If the timeout is
