$RodHat_
Console Tips

Process supervision is a 40-line problem. It has been solved since 1997.

Published by

Process supervision is a 40-line problem. It has been solved since 1997.
Photo: AI-generated — no human photographer / RodHat AI Cover

The problem is genuinely simple: keep a process running, restart it if it dies, don’t restart it so fast you melt the box, capture its output, and shut it down cleanly when asked. Dan Bernstein wrote software that does exactly that in 1997, it fit in a handful of small C programs, and the design has been quietly correct ever since.

You can use it. More usefully, you can understand why it’s shaped the way it is, because that shape is the actual lesson.

The idea: the daemon does not daemonize

Every supervision system that works is built on one inversion. Traditional Unix daemons fork, detach from the terminal, close their fds, write a pidfile, and disappear. Which means the thing that started them has no idea whether they’re alive, and the only handle on them is a text file containing a number that goes stale the instant the process dies and the PID gets reused.

Supervision flips it. The service runs in the foreground as a direct child of the supervisor. No fork, no pidfile, no detaching. Now the supervisor learns about death from wait(2) — instantly, reliably, with the exit status — instead of polling a pidfile and hoping. That’s it. That’s the whole architectural insight, and every correct answer since has been a variation on it.

If you take nothing else from this: when you write a daemon, give it a foreground mode and make it the default. -D, --foreground, --no-daemonize. Any modern supervisor, container runtime, or process manager needs it, and a daemon that insists on backgrounding itself is unmanageable in 2026.

runit, concretely

A service is a directory with an executable run script in it:

mkdir -p /etc/sv/myapp/log

/etc/sv/myapp/run:

#!/bin/sh
exec 2>&1
exec chpst -u app:app /usr/local/bin/myapp --foreground --config /etc/myapp.conf

Three things in three lines. exec 2>&1 folds stderr into stdout so both get logged. chpst -u drops privileges. The final exec replaces the shell with the daemon — so the process the supervisor is watching is your application, not a shell that happens to have spawned it. Miss that exec and signals go to the shell, which ignores them, and your service becomes unkillable-by-normal-means. It’s the single most common mistake in these files.

/etc/sv/myapp/log/run:

#!/bin/sh
exec svlogd -tt /var/log/myapp

The supervisor pipes the service’s stdout into the log service’s stdin. svlogd timestamps every line, rotates by size, and keeps N files. No logrotate, no SIGHUP dance, no deleted-but-open log file eating the disk — the pipe means the writer and the rotator are the same program and rotation is just closing one file and opening another.

Enable it:

ln -s /etc/sv/myapp /var/service/

That’s the installation. A symlink. runsvdir scans that directory and starts supervising within five seconds.

Control it:

sv status myapp
sv restart myapp
sv down myapp
sv once myapp

The parts people skip

Restart backoff. A service that crashes on startup and gets restarted immediately, forever, is a fork bomb with better branding. runit sleeps a second between restarts; if your failure is “config file is wrong” you want more. Put it in the run script:

#!/bin/sh
sleep 1
exec 2>&1
exec myapp --foreground

Crude and effective — the sleep is inside the supervised process, so it applies to every restart by construction.

Dependencies. runit’s answer is: block in the run script until your dependency is up.

sv check postgres || exit 1

If it fails, the script exits, the supervisor restarts it a second later, and it tries again. That’s not a hack — it’s a retry loop that converges, and it doesn’t require the supervisor to model a dependency graph it can’t actually verify. A service that can’t survive its database being briefly absent is going to fall over the first time the database restarts anyway.

Clean shutdown. sv down sends SIGTERM then SIGCONT. If your app needs something else, /etc/sv/myapp/control/t is a script that runs instead. One file, one signal, no unit-file directive to look up.

What you give up

Honestly: socket activation, cgroup resource limits without extra tooling, and a large ecosystem of pre-written unit files. Those are real. If you need per-service memory caps and you’re on Linux, you’re going to end up writing cgroup rules by hand or reaching for the thing I don’t use.

What you get is a service definition you can read in fifteen seconds, in a language you already know, that does exactly what it says — and a supervisor whose entire source you could read on a plane.

The run script is the point. It’s a shell script. When it misbehaves you run it by hand and watch what happens. There is no layer where the behavior is defined by a directive whose semantics you have to go look up.