Deep Dive: OpenRC Service Management & Debugging
OpenRC is simpler than systemd. That’s both its strength and its trap—when things break, you’re on your own.
Why OpenRC?
When I started building Argo OS, people asked: “Why not just use systemd like everyone else?”
The honest answer: I understand OpenRC. I don’t understand systemd.
That’s not a criticism of systemd. It’s a powerful, capable init system. But it’s also complex, opinionated, and does a lot of magic behind the scenes. When something breaks in systemd, I spend hours reading man pages and stack overflow posts trying to understand what went wrong.
OpenRC is shell scripts. When something breaks, I read the shell script. The problem is usually obvious.
The practical answer:
| Feature | systemd | OpenRC |
|---|---|---|
| Service scripts | Unit files (declarative) | Shell scripts (imperative) |
| Parallelization | Aggressive | Conservative (configurable) |
| Socket activation | Native | Possible but rare |
| Learning curve | Steep | Gentle |
| Debugging | journalctl + systemctl | Read the script |
| Distro integration | Deep | Minimal |
For a desktop system where I want to understand everything, OpenRC wins. For a server with 50 services and complex dependencies, systemd might be better.
The SDDM Login Crisis
I learned OpenRC the hard way when KDE Plasma refused to start.
The Symptoms
- Boot completes normally
- SDDM appears (the login screen)
- I enter my password
- Screen goes black for 2 seconds
- SDDM reappears
- Repeat forever
No error on screen. No crash dialog. Just an infinite login loop.
The Investigation
Step 1: Check the SDDM log
cat /var/log/sddm.log
[14:23:45.123] Auth: sddm-helper exited with 1
[14:23:45.124] Auth: User session terminated
Unhelpful. “Exited with 1” means “something failed.” Thanks.
Step 2: Check the Xorg log
cat /var/log/Xorg.0.log | grep -i error
Nothing. Xorg started fine. The X server wasn’t the problem.
Step 3: Check the user’s session log
cat ~/.local/share/sddm/xorg-session.log
startkde: Could not start D-Bus. Check if dbus is running.
There it is. KDE’s startup script couldn’t connect to D-Bus.
Step 4: Check if D-Bus is running
rc-service dbus status
* status: started
It’s running. But KDE can’t see it.
Step 5: Check elogind
SDDM uses elogind (the systemd-logind fork) to manage user sessions. Elogind needs D-Bus.
rc-service elogind status
* status: started
Also running. But…
loginctl
Failed to connect to bus: No such file or directory
Elogind is running, but it’s not connected to D-Bus. That’s the bug.
The Root Cause
OpenRC starts services in parallel (mostly). D-Bus started. Elogind started. But elogind started before D-Bus was fully ready to accept connections.
D-Bus was “started” according to OpenRC (the process was running), but the socket wasn’t listening yet. Elogind tried to connect, failed silently, and ran in a degraded state.
SDDM authenticated my password correctly, but when it tried to create a session via elogind, elogind couldn’t register it with D-Bus. Session creation failed. SDDM showed the login screen again.
The Fix
The fix is dependency ordering. Elogind needs to start after D-Bus is fully ready, not just after D-Bus’s process is spawned.
Edit /etc/init.d/elogind:
depend() {
need dbus
after dbus
}
The need keyword means “start this first.” The after keyword means “wait until it’s fully ready.”
But that’s not enough. We also need to ensure the display manager waits for elogind:
Edit /etc/init.d/display-manager:
depend() {
need dbus
need elogind
after dbus elogind
want xdm-setup
}
Then restart everything:
rc-service dbus restart
rc-service elogind restart
rc-service display-manager restart
It worked. The login loop was gone.
Total debugging time: 6 hours.
Understanding OpenRC Dependencies
OpenRC has four dependency keywords:
need
“This service is required. Start it before me, and stop it after me.”
depend() {
need net
}
If net fails to start, this service won’t start.
use
“If this service is available, start it before me.”
depend() {
use dns
}
If dns exists and is enabled, start it first. If it doesn’t exist, proceed anyway.
after
“If this service is starting, wait for it to finish before I start.”
depend() {
after localmount
}
Doesn’t require localmount to be enabled, but if it’s running, wait for it.
before
“Start me before these services.”
depend() {
before display-manager
}
Forces this service to start before the display manager, even if the display manager doesn’t know about it.
The Common Pattern
For a desktop service that needs D-Bus and network:
depend() {
need dbus
need net
after localmount
}
For a display manager:
depend() {
need dbus
need elogind
after dbus elogind
before xdm-setup
}
Writing Custom Init Scripts
The template for a robust OpenRC service:
#!/sbin/openrc-run
# /etc/init.d/myservice
name="My Service"
description="Does something useful"
# The command to run
command="/usr/bin/my-daemon"
command_args="--config /etc/myservice.conf"
command_user="myuser"
command_group="mygroup"
# For daemons that background themselves
command_background="yes"
pidfile="/run/myservice.pid"
# Logging
output_log="/var/log/myservice.log"
error_log="/var/log/myservice.err"
# Dependencies
depend() {
need net
after firewall
}
# Optional: custom start logic
start_pre() {
checkpath --directory --owner myuser:mygroup /var/lib/myservice
}
# Optional: custom stop logic
stop_post() {
rm -f /run/myservice.sock
}
Key Fields
command: The binary to run.
command_args: Arguments passed to the command.
command_user: Drop privileges to this user (security best practice).
command_background: If the daemon doesn’t fork itself, OpenRC will background it.
pidfile: How OpenRC tracks the process. Required for proper stop/restart.
The start_pre Hook
Runs before the service starts. Use for:
- Creating required directories
- Checking configuration validity
- Setting permissions
start_pre() {
# Ensure config exists
if [ ! -f /etc/myservice.conf ]; then
eerror "Config file missing!"
return 1
fi
# Ensure runtime directory exists
checkpath --directory --owner myuser:mygroup --mode 0755 /run/myservice
}
The stop_post Hook
Runs after the service stops. Use for:
- Cleaning up socket files
- Removing stale PID files
- Sending notifications
stop_post() {
rm -f /run/myservice.sock
rm -f /run/myservice.pid
}
Debugging Crashed Services
Finding Crashed Services
rc-status
Shows all services and their states. Look for “crashed” or “stopped.”
rc-status -c
Shows only crashed services.
The ZAP Command
If a service crashed but OpenRC still thinks it’s running, you can’t start it:
rc-service myservice start
* WARNING: myservice has already been started
But the process is dead. Use zap to reset the state:
rc-service myservice zap
rc-service myservice start
zap doesn’t try to stop the process—it just tells OpenRC “forget about the current state.”
Verbose Startup
To see what’s happening during start:
rc-service -v myservice start
Even more verbose:
rc-service -v -D myservice start
Reading Service Logs
Most services log to /var/log/. Common locations:
| Service | Log Location |
|---|---|
| SDDM | /var/log/sddm.log |
| Xorg | /var/log/Xorg.0.log |
| OpenRC | /var/log/rc.log (if enabled) |
| D-Bus | dbus-daemon --system logs to syslog |
| elogind | syslog or journald |
For services without dedicated logs, check syslog:
tail -100 /var/log/messages | grep myservice
The Post-Update D-Bus Problem
A recurring issue: after updating sys-apps/dbus, things break.
The Symptoms
- Services stop communicating
- GUI applications can’t launch
dbus-sendcommands fail
The Cause
When you update D-Bus, the binary changes on disk. But the running D-Bus daemon is the old binary. New clients try to speak a new protocol to an old daemon.
The Fix
Restart D-Bus. But be careful—restarting D-Bus kills everything connected to it, including your desktop session.
Safe approach:
- Log out of your desktop
- Switch to a TTY (Ctrl+Alt+F2)
- Log in as root
- Restart D-Bus
rc-service dbus restart
rc-service elogind restart
rc-service display-manager restart
- Switch back to graphical (Ctrl+Alt+F7)
- Log in
Safer approach: Just reboot after D-Bus updates. It’s faster than debugging a half-broken session.
Runlevels
OpenRC uses runlevels to group services. The main ones:
| Runlevel | Purpose |
|---|---|
| sysinit | Basic system initialization (udev, devfs) |
| boot | Boot-time services (localmount, bootmisc) |
| default | Normal services (networking, display-manager) |
| shutdown | Services that run at shutdown |
Adding a Service to a Runlevel
rc-update add myservice default
Removing a Service
rc-update del myservice default
Listing Services in a Runlevel
rc-update show default
The Boot Sequence
- Kernel starts
/sbin/init - OpenRC runs
sysinitrunlevel - OpenRC runs
bootrunlevel - OpenRC runs
defaultrunlevel - Login prompt (or display manager)
Services in earlier runlevels start before later ones. Within a runlevel, dependencies determine order.
Common Gotchas
1. Network Dependency
Many services need need net. But what does “net” mean?
On OpenRC, net is satisfied by any network interface being up. This includes lo (loopback). If your service needs actual network connectivity, check for it explicitly:
start_pre() {
# Wait for actual network (not just loopback)
while ! ping -c 1 8.8.8.8 &>/dev/null; do
sleep 1
done
}
2. Service Name vs Script Name
The service name is the file name in /etc/init.d/. If you create /etc/init.d/my-service, the service is my-service, not myservice.
rc-service my-service start # Correct
rc-service myservice start # Wrong (unless symlinked)
3. Permissions on Init Scripts
Init scripts must be executable:
chmod +x /etc/init.d/myservice
If you forget this, OpenRC will silently ignore the service.
4. The Parallel Trap
OpenRC can start services in parallel for faster boot. But parallel startup can cause race conditions (like my SDDM bug).
To disable parallel startup for debugging:
# /etc/rc.conf
rc_parallel="NO"
Once you’ve fixed dependencies, you can re-enable it.
My Init Scripts Collection
Services I’ve written for Argo OS:
/etc/init.d/swarm-drone — Build swarm drone daemon
#!/sbin/openrc-run
description="Gentoo Build Swarm Drone"
command="/opt/swarm/drone/main.py"
command_args="--config /etc/swarm/drone.conf"
command_user="root"
command_background="yes"
pidfile="/run/swarm-drone.pid"
output_log="/var/log/swarm-drone.log"
error_log="/var/log/swarm-drone.err"
depend() {
need net
after firewall tailscaled
}
/etc/init.d/rclone-mount — Mount cloud storage
#!/sbin/openrc-run
description="Rclone FUSE mount"
command="/usr/bin/rclone"
command_args="mount secret:vault /mnt/vault --allow-other --vfs-cache-mode full"
command_background="yes"
pidfile="/run/rclone-mount.pid"
depend() {
need net fuse
after firewall
}
stop() {
fusermount -u /mnt/vault
}
The Philosophy
OpenRC is a tool, not a solution. It starts processes in order and stops them in reverse. That’s it.
This simplicity means:
- I understand every service on my system
- Debugging is “read the script, find the bug”
- No magic, no surprises
The downside:
- Socket activation doesn’t work well
- Aggressive parallelism requires careful dependencies
- Less ecosystem support than systemd
For my desktop, it’s the right choice. For a complex server, I might think differently.
Related: Building Argo OS, KDE Plasma on OpenRC.