Containers & Linux
2026
sentinel
Your program runs as PID 1 in a container. The kernel drops any signal PID 1 has no handler for, so docker stop does nothing and the container gets SIGKILLed.
- A container under sentinel exits about 3 ms after SIGTERM. The same program alone on PID 1 ignores the signal and gets SIGKILLed at the ten second deadline.
- It restarts a dead child on a 1, 2, 4, 8, 16 second backoff, and serves restart, crash and failure counters for Prometheus to scrape.
- 13 ctest cases and 5 CI jobs. CI stands the whole Compose stack up and fails unless Prometheus is scraping and Grafana has the dashboard.
- C++17
- Linux
- POSIX signals
- Docker Compose
- Prometheus
- Grafana
- GitHub Actions
What PID 1 costs you
- Shutdown after SIGTERM
- 2.5 to 3.1 ms
- Child that ignores SIGTERM
- 5.003 s
- Restart backoff
- 1, 2, 4, 8, 16 s
- Test suite
- 13 of 13
- Supervisor footprint
- 3.9 MB RSS
Seven runs, worker supervised by sentinel on PID 1 of a PID namespace, timed through wait() so there is no polling error. The same worker alone was still alive at the ten second deadline and needed SIGKILL.
Same harness, worker in stubborn mode, three runs inside 1 ms of each other. That is the SIGKILL escalation firing on schedule.
Timed from the supervisor's own log against a child that dies at once. Five restarts in forty seconds, and the pause caps at sixteen.
ctest on a Release build, 33 s. CI runs build, test, shellcheck, docker and compose as separate jobs.
Read from /proc while supervising, on two threads. The binary is 36 KB, or 31 KB stripped.
The kernel will not deliver a signal to PID 1 unless PID 1 installed a handler for it. So the container sits through the whole grace period and then dies by SIGKILL. sentinel takes the PID 1 slot and lets your program run as an ordinary child. It exits with the child's status code, or 128 plus the signal number if the child was killed. Exit 137 out of a container is 128 plus 9. That is the OOM killer.
The handlers go in without SA_RESTART. That matters: with the flag, the kernel restarts waitpid after a signal and the reap loop never gets a turn. Without it, waitpid returns EINTR and the loop keeps control. The handler calls kill() and nothing else. Very little is safe to call from signal context. Escalation lives under the same constraint. The SIGTERM handler arms alarm(5), and a SIGALRM handler does the killing.
There used to be a window between the fork and the line that installed the handlers. A SIGTERM landing in it killed sentinel and left the child running as an orphan. That is the exact failure sentinel exists to prevent. I marked it as a TODO and lived with it for a while. The fix is sigprocmask around the fork. It blocks SIGINT and SIGTERM before, and unblocks once the handlers are up. The child restores the original mask before exec. Otherwise everything sentinel launches starts out deaf to those signals.
The metrics server needs its own thread, because the main one sits blocked in waitpid and cannot also wait in accept. That thread blocks every signal for itself. SIGTERM has to keep landing on the main thread where the handler lives, or waitpid never gets its EINTR. The counters are atomic since both threads touch them.
One compose command brings up sentinel, a Prometheus scraping it every 5 s, and a Grafana with the dashboard already provisioned. The stack runs a child that crashes on purpose, so the graphs have something real on them. Two alert rules ship with it, one for a crashloop and one for a child that has been down two minutes. CI stands the whole thing up on every push and fails unless Prometheus reports the target up, the restart counter has moved, both rules loaded, and Grafana is serving the dashboard.
