dumb-init
A minimal init system that runs as PID 1 in containers, correctly forwarding signals and reaping zombie processes.
Repository Health
Technical Analysis
dumb-init is a lightweight, statically-linked init system written in C, designed to run as PID 1 inside minimal container environments like Docker. It solves two problems that plague single-process containers: signals sent to PID 1 are silently ignored unless explicitly handled, and orphaned child processes become unreaped zombies. By prefixing a container’s entrypoint with dumb-init, the target process runs as a proper child instead of PID 1, so it receives default signal behavior while dumb-init forwards signals and reaps zombies on its behalf.
Distributed as a small C binary, dumb-init is available via apt packages, direct .deb/binary download, or a PyPI wheel that compiles and installs the same executable. It supports two signal-forwarding modes — a default session-rooted mode that proxies signals to the whole process group (useful for shell scripts that don’t forward signals to their own children) and a —single-child mode for fully transparent, chainable behavior — plus configurable signal rewriting for supervisors that only ever send one standard stop signal.
What You Get
- Proper signal handling for containerized processes, so SIGTERM actually stops your app instead of being silently dropped
- Automatic reaping of orphaned zombie processes re-parented to PID 1
- A statically-linked binary with no runtime dependencies, as small as ~20KB when built with musl
- Configurable signal rewriting via —rewrite for apps that expect a nonstandard stop signal
- Multiple installation paths: distro apt packages, a downloadable .deb, a raw static binary, or a PyPI wheel
Common Use Cases
- Docker ENTRYPOINT wrapping for any containerized application
- Kubernetes or Mesos deployments that need consistent, graceful SIGTERM handling
- Shell script entrypoints that spawn background processes needing shared signal propagation
- CI jobs using
docker runthat must remain interruptible with Ctrl+C or SIGTERM
Under The Hood
Architecture
dumb-init is a single ~340-line C file implementing a small, flat state machine rather than an object model. parse_command() handles CLI flags and builds a signal-rewrite table from globals like signal_rewrite[]; main() blocks all signals with sigprocmask(), installs a dummy handler for signals 1-31, forks the wrapped command as a child, and loops on sigwait() dispatching into handle_signal(), which either reaps exited children via waitpid() on SIGCHLD or translates and forwards the signal via translate_signal()/forward_signal() and kill(). There’s no layering or dependency injection — the entire behavior lives in a handful of top-level functions operating on shared global state (child_pid, use_setsid, signal_rewrite[]), which is a deliberate, appropriate tradeoff for a program whose whole job is proxying signals to one child, though it means any change to signal semantics touches both main() and handle_signal() directly.
Tech Stack
The project is pure C, buildable against either glibc (the default) or musl (CC=musl-gcc) for a dramatically smaller static binary — roughly 700KB with glibc versus around 20KB with musl. Building uses a plain Makefile, and the same source is packaged three ways: a Debian package built from the debian/ control files via dpkg-buildpackage, static binaries attached to GitHub Releases, and a PyPI wheel produced by a setup.py that layers custom bdist_wheel/build_cexe/install_cexe distutils commands on top of setuptools to compile and bundle the C binary rather than expose any Python API. CI runs through both circle.yml and GitHub Actions.
Code Quality
Tests live under tests/ (roughly 720 lines across files like cli_test.py, proxies_signals_test.py, exit_status_test.py, and tty_test.py) and run with pytest (pytest.ini sets a 20-second timeout per test), exercising the compiled binary end-to-end via subprocess rather than unit-testing individual C functions — an appropriate black-box style for a signal-handling program. A pre-commit config enforces autopep8 formatting plus hygiene hooks (trailing whitespace, merge-conflict markers, private-key detection) on the Python side; the C source itself has no linter or sanitizer configured beyond whatever warnings the Makefile’s compiler flags produce. Error handling in the C code favors defensive assert() calls over graceful degradation, which is reasonable for a small init process where a bug should fail loudly rather than limp along.
What Makes It Unique
dumb-init doesn’t invent a new mechanism — it applies the well-understood Unix pattern of session leader plus signal proxy plus zombie reaper to the specific problem Docker’s flat process model creates. Its most distinguishing design choice is the signal-rewrite table (--rewrite s:r), which lets one generic init binary adapt to processes expecting nonstandard stop signals without wrapping them in bespoke shell shims, alongside the deliberate rewrite of SIGTSTP/SIGTTIN/SIGTTOU to SIGSTOP that restores normal shell job-control behavior inside a session that would otherwise silently drop those signals. It plays the same role as tini, its closest alternative, but the rewrite mechanism is unusual among comparable tools.