Skip to main content

Supervice

License: Apache 2.0 Python 3.10+ Code style: ruff Type checked: mypy Documentation

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.


Features

  • Async-first — Built entirely on asyncio for efficient I/O multiplexing
  • Zero 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-in RotatingFileHandler with configurable size and backup count
  • Uptime 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, passes mypy --strict

Installation

Requirements: Python 3.10+ on a Unix-like OS (Linux, macOS).

# From source
git clone https://github.com/yourusername/supervice.git
cd supervice
pip install .

# Development install (includes docs dependencies)
pip install -e ".[docs]"

Quick Start

1. Create a configuration file

# supervisord.conf
[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

2. Start the daemon

# Foreground (development)
supervice -c supervisord.conf -n

# Background (production — default)
supervice -c supervisord.conf

3. Control processes

# Check status
supervicectl status

# Output:
# NAME                 STATE      PID        UPTIME
# --------------------------------------------------------
# webapp               RUNNING    12345      1:23:45
# worker:00            RUNNING    12346      1:23:44
# worker:01            RUNNING    12347      1:23:44
# worker:02            RUNNING    12348      1:23:44
# worker:03            RUNNING    12349      1:23:44

# Start / stop / restart individual processes
supervicectl stop worker:00
supervicectl start worker:00
supervicectl restart worker:00
supervicectl restart worker:00 --force   # SIGKILL instead of graceful

# Group operations
supervicectl stopgroup workers
supervicectl startgroup workers

# Hot reload (add/remove programs without restart)
supervicectl reload

# Use a custom socket path
supervicectl -s /var/run/supervice.sock status

Configuration Reference

[supervice] — Global settings

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

[program:NAME] — Process definitions

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 docs
reconcile auto Orphans of a crashed supervisor found at startup: auto, kill, warn, off. Matches on identity, not pid — see docs
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)

[group:NAME] — Process groups

Option Default Description
programs (required) Comma-separated list of program names

Health check options (per program)

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

Process States

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

Architecture

┌─────────────────────────────────────────────────┐
│                   supervice                      │
│                                                  │
│  ┌──────────┐    ┌───────────┐    ┌──────────┐  │
│  │  Config   │───▶│ Supervisor │───▶│ Process  │  │
│  │  Parser   │    │   (core)   │    │ Manager  │  │
│  └──────────┘    └─────┬─────┘    └────┬─────┘  │
│                        │               │         │
│                   ┌────▼────┐    ┌─────▼─────┐  │
│                   │   RPC    │    │  EventBus  │  │
│                   │  Server  │    │  (pub/sub) │  │
│                   └────┬────┘    └───────────┘  │
│                        │                         │
└────────────────────────┼─────────────────────────┘
                         │ Unix Socket
                    ┌────▼────┐
                    │supervice│
                    │  ctl    │
                    └─────────┘

Platform Support

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 pdeathsig — no kernel equivalent exists; supervice logs a warning if you request it. Children survive an abrupt supervisor kill

Field notes from the first production FreeBSD deployment live in PORTABILITY-FREEBSD.md.

FreeBSD: rc.d integration

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
# PROVIDE: supervice
# REQUIRE: LOGIN
# KEYWORD: shutdown

. /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, set pidfile = nonedaemon(8) writes it as root before dropping privileges, so a second writer fails with EPERM/EACCES.
  • If you run supervice under daemon(8), start it with -r (restart on death) and -P (pidfile holds daemon's own pid, not the child's): pointing the pidfile at the child means service 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

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.

Documentation

Full documentation is available at supervice.readthedocs.io.

Development

# Run tests
python3 -m pytest tests/ -v

# Type checking (strict mode)
mypy --strict supervice/

# Linting
ruff check supervice/

# Formatting
ruff format supervice/

# Build documentation locally
pip install -e ".[docs]"
cd docs && make html

License

Apache License 2.0 — see LICENSE for details.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

supervice-0.4.1.tar.gz (81.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

supervice-0.4.1-py3-none-any.whl (52.8 kB view details)

Uploaded Python 3

File details

Details for the file supervice-0.4.1.tar.gz.

File metadata

  • Download URL: supervice-0.4.1.tar.gz
  • Upload date:
  • Size: 81.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.3

File hashes

Hashes for supervice-0.4.1.tar.gz
Algorithm Hash digest
SHA256 49dafab209038d13ce88179d97721c920859635a3ef1e82efae7e914dacbb263
MD5 4d710f1a65df0f1988210a9a89f3163b
BLAKE2b-256 6084d7f6c95abe9d343067f2622406dd96c040811854d47420c6d345af8f0def

See more details on using hashes here.

File details

Details for the file supervice-0.4.1-py3-none-any.whl.

File metadata

  • Download URL: supervice-0.4.1-py3-none-any.whl
  • Upload date:
  • Size: 52.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.3

File hashes

Hashes for supervice-0.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 060a69d8cce174fcb5c6fd6d44ce4d196355db06988697e3e5d5550fa9412a96
MD5 467073254ea0aa7911dfab8cc62bd832
BLAKE2b-256 92949b81c2a409a54ab57bb1d4fe224dd99784161c70947df4d3f27952460c64

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page