The init system is the first process the kernel starts (PID 1). It brings up the rest of the system, starts and stops services, and reaps orphaned processes until shutdown. Most mainstream distributions use systemd today, but SysVinit and OpenRC are still in active use on Devuan, Alpine, Gentoo and embedded systems. This guide compares the three on design, day-to-day commands, logging, dependencies and resource control, and shows the same service written for each one so you can judge the differences yourself.
Which init system does your server use?
Before comparing, check what is running as PID 1 on your machine:
ps -p 1 -o comm=
systemd
On a SysVinit system this prints init. On OpenRC systems PID 1 is usually also init (SysVinit's or BusyBox's), because OpenRC is a service manager that runs on top of an existing init binary. You can confirm OpenRC by checking for its script runner:
command -v openrc-run
/sbin/openrc-run
At a glance
| SysVinit | systemd | OpenRC | |
|---|---|---|---|
| Service definition | Shell scripts in /etc/init.d/ | Declarative unit files (.service, .timer, .socket...) | Shell scripts using #!/sbin/openrc-run with helper variables |
| Startup ordering | Numbered symlinks per runlevel (S01, S02...) or LSB header dependencies | Dependency graph (Requires=, Wants=, After=), parallel by default | depend() function (need, use, after), optional parallel start |
| Process supervision | None for services (only respawn entries in /etc/inittab) | Built in (Restart=on-failure) | Optional via supervise-daemon |
| Logging | Whatever the daemon writes, usually syslog | journald (binary journal, journalctl) plus optional syslog forwarding | Syslog (for example BusyBox syslogd on Alpine) |
| Resource limits | ulimit inside the script | cgroups v2 per unit (MemoryMax=, CPUQuota=) | cgroups through rc_cgroup_* settings |
| Scheduled jobs | cron | systemd timers or cron | cron |
| Typical distributions | Devuan, Slackware, older Debian/RHEL, embedded | Ubuntu, Debian, RHEL, Rocky, Fedora, Arch, SUSE | Alpine, Gentoo, Artix, Devuan (optional) |
SysVinit
SysVinit reads /etc/inittab, enters a runlevel (commonly 2, 3 or 5 for multi-user, 0 for halt and 6 for reboot) and runs the scripts linked into /etc/rcN.d/. Scripts whose names start with S are called with start, those starting with K with stop, in alphabetical order. Each script is a plain shell program that must handle start, stop, restart and status itself.
Strengths:
- Very small and easy to reason about: everything is a shell script you can read and run by hand.
- No dependencies beyond a shell, which suits tiny embedded images.
Weaknesses:
- Services start one after another, so boot is slower on systems with many services.
- No supervision: if a daemon crashes it stays down until someone restarts it.
- PID file handling and daemonization are reimplemented, often badly, in every script.
Daily commands on a Debian-style SysVinit system:
sudo service nginx start
sudo service nginx status
sudo update-rc.d nginx defaults
sudo update-rc.d nginx disable
RHEL-style systems before version 7 used chkconfig nginx on instead of update-rc.d.
systemd
systemd treats everything it manages as a unit: services, mount points, sockets, timers and targets (the equivalent of runlevels, such as multi-user.target). It builds a dependency graph from the unit files and starts everything it can in parallel. Each service runs in its own cgroup, so systemd always knows which processes belong to it, can restart it on failure and can apply CPU, memory and I/O limits.
Strengths:
- Declarative unit files, so there is no shell boilerplate to get wrong.
- Supervision, restart policies, sandboxing (
ProtectSystem=,PrivateTmp=) and resource control built in. - Structured logging with
journalctl, filtered by unit, boot, priority or time. - Timers, socket activation and boot analysis tools in the same toolset.
Weaknesses:
- Large scope: it also covers logging, networking (
systemd-networkd), DNS (systemd-resolved) and more, which some administrators consider too much for PID 1. - Linux only and tied to glibc, so it is not an option on musl-based distributions such as Alpine.
Daily commands:
sudo systemctl start nginx
sudo systemctl status nginx
sudo systemctl enable --now nginx
sudo systemctl disable nginx
sudo systemctl reload nginx
OpenRC
OpenRC is a dependency-based service manager originally written for Gentoo. It keeps the familiar /etc/init.d/ scripts but replaces the boilerplate with a small framework: the script sets variables such as command and pidfile, declares dependencies in a depend() function, and openrc-run does the rest. Runlevels are named directories (boot, default, shutdown) under /etc/runlevels/.
Strengths:
- Much shorter scripts than SysVinit, with real dependency resolution.
- Small and portable: it runs on Linux with glibc or musl and on BSDs.
- Optional supervision with
supervise-daemon, and optional parallel startup.
Weaknesses:
- Smaller ecosystem: upstream projects ship systemd units far more often than OpenRC scripts, so you may have to write your own.
- No built-in structured logging or timers.
Daily commands on Alpine or Gentoo:
sudo rc-service nginx start
sudo rc-service nginx status
sudo rc-update add nginx default
sudo rc-update del nginx default
rc-status
The same service written for each init system
The clearest way to see the difference is to define one service three times. Assume a program at /usr/local/bin/myapp that runs in the foreground, listens on port 8080 and should run as the unprivileged user myapp.
systemd unit
Create the unit file:
sudo nano /etc/systemd/system/myapp.service
[Unit]
Description=My application
After=network-online.target
Wants=network-online.target
[Service]
User=myapp
Group=myapp
ExecStart=/usr/local/bin/myapp --port 8080
Restart=on-failure
RestartSec=5
MemoryMax=512M
CPUQuota=50%
[Install]
WantedBy=multi-user.target
Load and start it:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
systemd handles the user switch, restarts the process if it exits with an error, and enforces a 512 MB memory ceiling and half a CPU through cgroups.
OpenRC script
sudo nano /etc/init.d/myapp
#!/sbin/openrc-run
name="myapp"
description="My application"
command="/usr/local/bin/myapp"
command_args="--port 8080"
command_user="myapp:myapp"
supervisor="supervise-daemon"
pidfile="/run/${RC_SVCNAME}.pid"
depend() {
need net
after firewall
}
sudo chmod +x /etc/init.d/myapp
sudo rc-update add myapp default
sudo rc-service myapp start
With supervisor="supervise-daemon", OpenRC keeps the process in the foreground and restarts it if it dies. Without that line it falls back to start-stop-daemon and no supervision.
SysVinit script
A SysVinit script has to do everything itself. On Debian-family systems the LSB header lets update-rc.d compute the start order:
sudo nano /etc/init.d/myapp
#!/bin/sh
### BEGIN INIT INFO
# Provides: myapp
# Required-Start: $network $remote_fs $syslog
# Required-Stop: $network $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: My application
### END INIT INFO
DAEMON=/usr/local/bin/myapp
PIDFILE=/run/myapp.pid
. /lib/lsb/init-functions
case "$1" in
start)
log_daemon_msg "Starting myapp" "myapp"
start-stop-daemon --start --background --make-pidfile --pidfile "$PIDFILE" \
--chuid myapp:myapp --exec "$DAEMON" -- --port 8080
log_end_msg $?
;;
stop)
log_daemon_msg "Stopping myapp" "myapp"
start-stop-daemon --stop --pidfile "$PIDFILE" --retry 10
log_end_msg $?
rm -f "$PIDFILE"
;;
restart)
"$0" stop
"$0" start
;;
status)
status_of_proc -p "$PIDFILE" "$DAEMON" myapp
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
exit 1
;;
esac
sudo chmod +x /etc/init.d/myapp
sudo update-rc.d myapp defaults
sudo service myapp start
There is no restart on crash and no memory limit. To add either you would need an external supervisor or ulimit calls in the script.
Logging
This is where daily work differs most.
With systemd, every unit's stdout and stderr goes to the journal automatically:
journalctl -u myapp -f
journalctl -u myapp --since "1 hour ago"
journalctl -b -p err
The last command shows only messages of priority err or worse since the current boot. On Ubuntu and Debian, journald also forwards to rsyslog when it is installed, so /var/log/syslog keeps working.
With SysVinit and OpenRC, logging is whatever the daemon does. Most daemons write to syslog, so you read /var/log/syslog, /var/log/messages (Alpine with BusyBox syslogd) or the program's own log file:
tail -f /var/log/messages
Dependencies and boot order
- SysVinit orders scripts by the numbers in their symlink names. The LSB header (
Required-Start:) lets tools such asinsservcompute those numbers, but it is still a linear sequence. - OpenRC resolves
need(hard dependency),use(soft dependency, start it if it is enabled) andafter/before(ordering only). Withrc_parallel="YES"in/etc/rc.confit starts independent services at the same time; it is off by default because parallel output is harder to read. - systemd separates requirement (
Requires=,Wants=) from ordering (After=,Before=). A common mistake is writingAfter=postgresql.servicewithoutWants=orRequires=, which orders the units but does not start PostgreSQL.
To see what slows down a systemd boot:
systemd-analyze
systemd-analyze blame | head
systemd-analyze critical-chain
Startup finished in 2.108s (kernel) + 6.472s (userspace) = 8.580s
graphical.target reached after 6.431s in userspace.
OpenRC and SysVinit have no equivalent tool. You measure boot time with timestamps in the logs or with bootchart.
Resource control and scheduling
systemd places every service in a cgroup and exposes the limits directly in the unit: MemoryMax=, CPUQuota=, IOWeight=, TasksMax=. You can change them live:
sudo systemctl set-property myapp.service MemoryMax=1G
systemd-cgtop
OpenRC can also place services in cgroups through the rc_cgroup_* settings documented in /etc/rc.conf, but the configuration is less granular. SysVinit has no cgroup support; you are limited to ulimit inside the script.
For scheduled jobs, systemd offers timers as an alternative to cron, with logging in the journal and Persistent=true to catch up on runs missed while the machine was off:
systemctl list-timers
On SysVinit and OpenRC systems you use cron.
Troubleshooting commands side by side
| Task | SysVinit | systemd | OpenRC |
|---|---|---|---|
| Service status | service myapp status | systemctl status myapp | rc-service myapp status |
| Failed services | Check logs manually | systemctl --failed | rc-status --crashed |
| Services enabled at boot | ls /etc/rc2.d/ | systemctl list-unit-files --state=enabled | rc-update show |
| Debug a start script | sudo sh -x /etc/init.d/myapp start | journalctl -u myapp -b | sudo /etc/init.d/myapp -d start |
| Check definition syntax | sh -n /etc/init.d/myapp | systemd-analyze verify myapp.service | sh -n /etc/init.d/myapp |
| Current runlevel/target | runlevel | systemctl get-default | rc-status --runlevel |
Migrating between init systems
- SysVinit to systemd: systemd runs old scripts from
/etc/init.d/throughsystemd-sysv-generator, so a missing unit file does not stop a service from working. Still, rewrite important services as native units to get supervision, cgroup limits and clean logging. Move theExecStart=command line out of the script, drop PID file handling and let the program run in the foreground. - SysVinit to OpenRC: replace the
caseblock withcommand,command_argsandpidfilevariables and translate the LSB header into adepend()function. Devuan supports both, so you can switch per system. - systemd to OpenRC: usually means changing distribution (for example to Alpine or Artix). Translate
ExecStart=intocommand/command_args,User=intocommand_user,Restart=intosupervisor="supervise-daemon"and replace timers with cron entries. Sandboxing options such asProtectSystem=have no direct equivalent.
Which one should you choose?
In practice the init system comes with the distribution, so the real decision is which distribution fits the job.
Choose systemd (Ubuntu, Debian, Rocky Linux) for general-purpose servers. It is what upstream software, documentation and configuration management tools target, and supervision, resource limits and the journal save real time in operations.
Choose OpenRC (Alpine, Gentoo) when you want a small, readable system, need musl or BSD compatibility, or build minimal images and appliances. Alpine with OpenRC is a common choice for lightweight VMs and routers.
Choose SysVinit only for legacy systems, very constrained embedded devices or when you deliberately run Devuan or Slackware. For new deployments it offers nothing that OpenRC does not do better.
Conclusion
SysVinit, systemd and OpenRC all start services at boot, but they differ in how much they do for you. systemd gives supervision, dependency resolution, logging and cgroup limits in one integrated toolset, OpenRC keeps shell scripts but removes most of their boilerplate, and SysVinit leaves everything to the script author. As next steps, write a hardened systemd unit for one of your own services with systemd-analyze security, replace a cron job with a systemd timer, or try Alpine Linux in a test VPS to get familiar with OpenRC.
