Init Systems Compared
What It Is
An init system is PID 1 -- the first userspace process started by the Linux kernel. It starts and supervises all other services. If PID 1 dies, the kernel panics. Every Linux system has one.
This page compares the four init systems most relevant to immutable/embedded Linux: systemd, runit, OpenRC, and s6 + s6-rc.
Why It Matters
The init system choice affects:
- Boot speed: How fast services start (parallel vs sequential)
- Reliability: What happens when a service crashes
- Image size: How much space the init system takes
- Complexity: How much you need to understand to operate it
- Compatibility: Which C libraries and platforms it supports
For an immutable OS like FortrOS, the init system is baked into the image. Changing it requires rebuilding. The choice is permanent until you decide to rebuild with something else.
The Contenders
systemd
What: A comprehensive system and service manager. PID 1, service supervisor, logging daemon, network manager, container runtime, device manager, and more.
Service definition: Declarative INI-style unit files.
Dependency model: Declarative (After=, Requires=, Wants=). Parallel
startup by default. Socket activation (service starts on first connection to
its socket).
Readiness: sd_notify protocol. Service sends READY=1 on a Unix socket.
Supervision: Restarts crashed services per policy (Restart=on-failure).
Size: ~10MB+ (binaries), requires glibc.
Used by: Ubuntu, Fedora, Arch, Debian, RHEL, SUSE -- most mainstream distros.
Best for: General-purpose Linux distributions where the broad feature set is an asset and glibc is already required.
runit
What: Minimal process supervisor. Three stages: one-time setup, service supervision, shutdown.
Service definition: A directory with a run shell script.
Dependency model: None. All services start simultaneously. If service A needs service B, A's run script must poll/wait.
Readiness: None built-in. Services are "ready" when their process starts, which may not mean they're accepting connections.
Supervision: Restarts crashed services immediately.
Size: ~50KB (binaries). Works with musl.
Used by: Void Linux, some Docker containers.
Best for: Simple systems where services have no ordering requirements and you want maximum simplicity.
OpenRC
What: Dependency-based init system using shell scripts. Started as Gentoo's init system.
Service definition: Shell scripts with a depend() function declaring
dependencies.
Dependency model: need (hard dependency), use (soft dependency),
after (ordering without dependency). Resolves graph, starts in levels.
Readiness: Limited. start-stop-daemon checks that the process is
running. No application-level readiness signal.
Supervision: Limited. Can restart on crash but not as robust as dedicated supervisors.
Size: ~500KB (binaries). Works with musl.
Used by: Gentoo, Alpine Linux (alongside busybox init).
Best for: Systems that need dependency ordering without systemd's complexity. Shell-based service definitions are easy to write and debug.
s6 + s6-rc
What: Two complementary tools. s6 is a process supervisor (like runit with readiness signaling). s6-rc is a service manager that adds dependency resolution on top of s6.
Service definition: Directories with files: type (oneshot/longrun),
run (the executable), dependencies (list of dependencies),
notification-fd (readiness FD number).
Dependency model: Full dependency graph with oneshots (run-once scripts) and longruns (supervised daemons). Atomic transitions between service states.
Readiness: notification-fd protocol. The supervised process writes a newline to a specific file descriptor when ready. s6-rc waits for this before starting dependent services.
Supervision: Restarts crashed longruns. Configurable restart policies.
Size: ~200KB total (all s6 + s6-rc binaries). Works with musl.
Used by: Embedded systems, container images, FortrOS.
Best for: Minimal images that need proper dependency ordering and readiness signaling without systemd's weight or glibc requirement.
Comparison Table
| Feature | systemd | runit | OpenRC | s6 + s6-rc |
|---|---|---|---|---|
| Dependency ordering | Yes | No | Yes | Yes |
| Readiness signaling | sd_notify | No | Limited | notification-fd |
| Parallel startup | Yes | Yes (unordered) | Partial | Yes (ordered) |
| Total binary size | ~10MB+ | ~50KB | ~500KB | ~200KB |
| musl compatible | No | Yes | Yes | Yes |
| Oneshot services | Yes | No | Yes | Yes |
| Socket activation | Yes | No | No | Yes (s6-ipcserver) |
| Logging | journald | svlogd | syslog | s6-log |
| Cgroup management | Yes | No | No | No |
| Timer/cron | systemd.timer | No | No | No |
| Network management | networkd | No | Yes | No |
| Learning curve | Moderate | Low | Low | Moderate |
| Documentation | Extensive | Minimal | Good | Technical |
Why FortrOS Chose s6 + s6-rc
Hard constraint: musl. FortrOS images are built with Buildroot using musl libc (not glibc). This eliminates systemd. Everything else is secondary to this constraint.
Need: dependency ordering. Services have real dependencies (WireGuard must be up before the maintainer joins the gossip mesh). runit's "start everything, let services figure it out" model would require retry loops in every service. s6-rc handles this cleanly.
Need: readiness signaling. The maintainer takes time to initialize (WireGuard setup, initial TreeSync pull). The reconciler must not start until the maintainer is truly ready. s6-rc's notification-fd gives real "ready" signals, not "process started" guesses.
Bonus: tiny footprint. ~200KB for the entire init system fits the immutable image philosophy. Less code = less attack surface = less to audit.