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:

FeaturesystemdOpenRC
Service scriptsUnit files (declarative)Shell scripts (imperative)
ParallelizationAggressiveConservative (configurable)
Socket activationNativePossible but rare
Learning curveSteepGentle
Debuggingjournalctl + systemctlRead the script
Distro integrationDeepMinimal

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

  1. Boot completes normally
  2. SDDM appears (the login screen)
  3. I enter my password
  4. Screen goes black for 2 seconds
  5. SDDM reappears
  6. 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:

ServiceLog Location
SDDM/var/log/sddm.log
Xorg/var/log/Xorg.0.log
OpenRC/var/log/rc.log (if enabled)
D-Busdbus-daemon --system logs to syslog
elogindsyslog 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-send commands 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:

  1. Log out of your desktop
  2. Switch to a TTY (Ctrl+Alt+F2)
  3. Log in as root
  4. Restart D-Bus
rc-service dbus restart
rc-service elogind restart
rc-service display-manager restart
  1. Switch back to graphical (Ctrl+Alt+F7)
  2. 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:

RunlevelPurpose
sysinitBasic system initialization (udev, devfs)
bootBoot-time services (localmount, bootmisc)
defaultNormal services (networking, display-manager)
shutdownServices 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

  1. Kernel starts /sbin/init
  2. OpenRC runs sysinit runlevel
  3. OpenRC runs boot runlevel
  4. OpenRC runs default runlevel
  5. 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.