A modern, lightweight, and fully async process supervisor for Unix-like systems. Zero dependencies. Pure Python. Production-ready.
Supervice manages long-running processes with automatic restart, health checking,
process grouping, hot config reload, and a Unix socket RPC interface — all built
on Python's asyncio
with no external packages.
Async-first— Built entirely onasyncio
for efficient I/O multiplexingZero dependencies— Pure Python stdlib; nothing to install beyond Python 3.10+** Process groups**— Organize related processes and control them as a unit** Health checks**— TCP connectivity and script-based health monitoring with auto-restart** Hot reload**— Add/remove programs without restarting the daemon (supervicectl reload
)Daemon mode— Proper double-fork daemonization with PID file locking** Graceful shutdown**— SIGTERM/SIGINT triggers orderly stop of all child processes** Process group kill**— Stops entire process trees, not just the main PID** Log rotation**— Built-inRotatingFileHandler
with configurable size and backup countUptime tracking— Per-process wall-clock uptime displayed in status output** Retry with backoff**— Configurable start retries with automatic FATAL state on exhaustion** User switching**— Run processes as a specific user (requires root)** Type-safe**— Fully type-hinted, passesmypy --strict
Requirements: Python 3.10+ on a Unix-like OS (Linux, macOS).
git clone https://github.com/yourusername/supervice.git
cd supervice
pip install .
pip install -e ".[docs]"
[supervice]
loglevel = INFO
logfile = supervice.log
pidfile = supervice.pid
[program:webapp]
command = python3 -u app.py
autostart = true
autorestart = true
stdout_logfile = webapp.log
stderr_logfile = webapp_err.log
[program:worker]
command = python3 -u worker.py
numprocs = 4
autostart = true
autorestart = true
stdout_logfile = worker_%(process_num)s.log
stderr_logfile = worker_err_%(process_num)s.log
supervice -c supervisord.conf -n
supervice -c supervisord.conf
supervicectl status
supervicectl stop worker:00
supervicectl start worker:00
supervicectl restart worker:00
supervicectl restart worker:00 --force # SIGKILL instead of graceful
supervicectl stopgroup workers
supervicectl startgroup workers
supervicectl reload
supervicectl -s /var/run/supervice.sock status
| Option | Default | Description |
|---|---|---|
logfile |
||
| (stdout) | ||
Daemon log file; empty logs to stdout in foreground, supervice.log when daemonized |
||
loglevel |
||
INFO |
||
Log level: DEBUG , INFO , WARNING , ERROR , CRITICAL |
||
pidfile |
||
supervice.pid |
||
Path to the PID/lock file; set to none (or empty) to disable |
||
socket |
||
| (runtime dir) | ||
RPC socket; defaults to $XDG_RUNTIME_DIR/supervice.sock (root: /run/supervice.sock , else ~/.supervice.sock ) |
||
shutdown_timeout |
||
30 |
||
| Seconds to wait for graceful shutdown | ||
log_maxbytes |
||
52428800 |
||
| Max log file size before rotation (bytes, 0 = no rotation) | ||
log_backups |
||
10 |
||
| Number of rotated log backup files to keep |
| Option | Default | Description |
|---|---|---|
command |
||
| (required) | ||
| Command to execute (supports shell-style quoting) | ||
numprocs |
||
1 |
||
Number of instances to run (creates NAME:00 , NAME:01 , ...) |
||
autostart |
||
true |
||
| Start automatically when daemon starts | ||
autorestart |
||
true |
||
| Restart automatically when process exits | ||
startsecs |
||
1 |
||
| Seconds a process must run to be considered successfully started | ||
startretries |
||
3 |
||
| Max consecutive start attempts before entering FATAL state | ||
stopsignal |
||
TERM |
||
Signal to send when stopping (TERM , INT , QUIT , KILL , etc.) |
||
stopwaitsecs |
||
10 |
||
| Seconds to wait after stop signal before sending SIGKILL | ||
stdout_logfile |
||
| (none) | ||
File for stdout (rotated by the daemon; supports %(process_num)s ) |
||
stderr_logfile |
||
| (none) | ||
File for stderr (rotated by the daemon; supports %(process_num)s ) |
||
stdout_logfile_maxbytes / stderr_logfile_maxbytes |
||
50MB |
||
| Child log rotation threshold (0 disables) | ||
stdout_logfile_backups / stderr_logfile_backups |
||
10 |
||
| Rotated child log backups to keep | ||
pdeathsig |
||
true |
||
| Linux/FreeBSD: SIGKILL the direct child if the supervisor dies. One generation only — grandchildren are never covered; see | ||
reconcile |
||
auto |
||
Orphans of a crashed supervisor found at startup: auto , kill , warn , off . Matches on identity, not pid — see |
||
environment |
||
| (none) | ||
Environment variables: KEY=VAL,KEY2="val with,comma" |
||
env_file |
||
| (none) | ||
Comma-separated KEY=VALUE secrets files (# comments, quotes stripped); read as the supervisor before the privilege drop. Later files win; environment overrides env_file |
||
directory |
||
| (none) | ||
| Working directory for the process | ||
user |
||
| (none) | ||
| Run as this user (requires root privileges) |
| Option | Default | Description |
|---|---|---|
programs |
||
| (required) | ||
| Comma-separated list of program names |
| Option | Default | Description |
|---|---|---|
healthcheck_type |
||
none |
||
Health check type: none , tcp , script |
||
healthcheck_interval |
||
30 |
||
| Seconds between health checks | ||
healthcheck_timeout |
||
10 |
||
| Seconds to wait for health check response | ||
healthcheck_retries |
||
3 |
||
| Consecutive failures before marking unhealthy | ||
healthcheck_start_period |
||
10 |
||
| Seconds to wait before starting health checks | ||
healthcheck_port |
||
| (none) | ||
TCP port to check (required for tcp type) |
||
healthcheck_host |
||
127.0.0.1 |
||
| TCP host to check | ||
healthcheck_command |
||
| (none) | ||
Script to run (required for script type) |
Example with health checks:
[program:api]
command = python3 -u api_server.py
autostart = true
autorestart = true
healthcheck_type = tcp
healthcheck_port = 8080
healthcheck_interval = 15
healthcheck_retries = 3
healthcheck_start_period = 5
STOPPED ──┐
EXITED ──┼──> STARTING ──> RUNNING ──> STOPPING ──> STOPPED
FATAL ──┤ │ │
BACKOFF ──┘ │ EXITED
▼
UNHEALTHY (health check failures)
│
▼
auto-restart (if autorestart=true)
| State | Description |
|---|---|
STOPPED |
|
| Process is not running (initial or manually stopped) | |
STARTING |
|
| Process has been spawned, waiting for confirmation | |
RUNNING |
|
| Process is running and healthy | |
BACKOFF |
|
| Process exited too quickly, waiting before retry | |
STOPPING |
|
| Stop signal sent, waiting for process to exit | |
EXITED |
|
| Process has exited (normally or abnormally) | |
FATAL |
|
| Process failed to start after exhausting retries | |
UNHEALTHY |
|
| Process is running but health checks are failing |
┌─────────────────────────────────────────────────┐
│ supervice │
│ │
│ ┌──────────┐ ┌───────────┐ ┌──────────┐ │
│ │ Config │───▶│ Supervisor │───▶│ Process │ │
│ │ Parser │ │ (core) │ │ Manager │ │
│ └──────────┘ └─────┬─────┘ └────┬─────┘ │
│ │ │ │
│ ┌────▼────┐ ┌─────▼─────┐ │
│ │ RPC │ │ EventBus │ │
│ │ Server │ │ (pub/sub) │ │
│ └────┬────┘ └───────────┘ │
│ │ │
└────────────────────────┼─────────────────────────┘
│ Unix Socket
┌────▼────┐
│supervice│
│ ctl │
└─────────┘
| Platform | Status |
|---|---|
| Linux | First-class — full feature set, including pdeathsig via prctl(2) (direct child only) |
| FreeBSD | Supported (15.x, 13.x) — all features, including pdeathsig via procctl(2) (direct child only); see the FreeBSD notes below |
| macOS | Supported for supervision, without — no kernel equivalent exists; supervice logs a warning if you request it. Children survive an abrupt supervisor killpdeathsig |
Field notes from the first production FreeBSD deployment live in PORTABILITY-FREEBSD.md.
Run the supervisor itself as root from rc.d
with no ${name}_user
and
no daemon -u
— FreeBSD's rc.subr
wraps the whole command in su -m
when ${name}_user
is set, so combining it with daemon -u <user>
runs
setuid
twice and fails with EPERM
(and daemon -f
swallows the error).
Let supervice drop privileges per program with its user =
directive instead.
A worked /usr/local/etc/rc.d/supervice
unit (adapted from production):
#!/bin/sh
. /etc/rc.subr
name="supervice"
rcvar="supervice_enable"
load_rc_config $name
command="/usr/local/bin/supervice"
command_args="-c /usr/local/etc/supervice.ini"
run_rc_command "$1"
; /usr/local/etc/supervice.ini
[supervice]
logfile = /var/log/supervice/supervice.log
; daemon(8) is NOT used with -p here; let supervice own its pidfile.
; If an outer supervisor already owns it, use: pidfile = none
pidfile = /var/run/supervice/supervice.pid
socket = /var/run/supervice/supervice.sock
[program:api]
command = /usr/local/bin/myapp
directory = /usr/local/myapp
user = myapp
startsecs = 3
startretries = 3
stopsignal = TERM
stopwaitsecs = 10
healthcheck_type = tcp
healthcheck_port = 8080
Notes for FreeBSD operators:
- If something else (e.g.
daemon(8) -p
) already writes the pidfile, setpidfile = none
—daemon(8)
writes it as root before dropping privileges, so a second writer fails withEPERM
/EACCES
. - If you run supervice under
daemon(8)
, start it with(restart on death) and-r
(pidfile holds daemon's-P
ownpid, not the child's): pointing the pidfile at the child meansservice stop
kills supervice and daemon immediately restarts it. - The pidfile and socket parent directories must exist and be writable before
supervice
starts; config load now fails with a clear message otherwise. - Export
HOME
if your program reads client certificates from$HOME/.postgresql
or similar (asyncpg does; a wrapper script can set it).
macOS has no kernel pdeathsig equivalent. pdeathsig = true
is accepted but inactive, and supervice logs one warning at config load naming the affected programs.
What this means in practice. There are two separate guarantees, and macOS has one of them:
| Linux / FreeBSD | macOS | |
|---|---|---|
| Child dies when the supervisor is killed | ||
yes (pdeathsig , direct child only) |
||
| no | ||
| Orphan is cleaned up at the next start | ||
yes (reconcile ) |
||
yes (reconcile ) |
So after an abrupt kill (SIGKILL
, OOM, panic) the children keep running until
you start supervice again. That window is real and unbounded — if the crash
happens at 02:00 and nobody restarts until morning, a queue consumer keeps
consuming for those hours. There is no kernel mechanism on macOS to close it;
the honest mitigation is to make restarts fast and automatic (a launchd
KeepAlive
job), not to assume the gap is small.
What macOS does get is that the restart no longer makes it worse. Reconciliation identifies the orphans and kills them before spawning replacements, so a crash-restart cycle does not accumulate duplicate workers. Measured on Darwin arm64: without it, four orphans became five across a restart; with it, the count returns to one.
This replaces the pkill -u ... -f
launcher snippet previously recommended
here, which matched on a command string rather than on identity and would kill
any process whose command line merely looked similar. If you still use a
launcher, note the exec
— without it the supervisor is a child of the script, adding a generation. The same rule applies to your own program wrappers; see pdeathsig scope.
Testing orphan behaviour on macOS: use a silent child. A program with a
stdout_logfile
is reaped by SIGPIPE
when the supervisor dies — by accident,
not by pdeathsig — so the obvious test passes while the guarantee is absent.
python3 tests/orphan_harness.py
does this correctly and keeps the false-positive case beside it.
Full documentation is available at supervice.readthedocs.io.
python3 -m pytest tests/ -v
mypy --strict supervice/
ruff check supervice/
ruff format supervice/
pip install -e ".[docs]"
cd docs && make html
Apache License 2.0 — see LICENSE for details.