FreeBSD's rc.d Init System: Scripts, Dependencies, and Service Ordering
How FreeBSD orders rc.d scripts, separates dependency order from enablement, and provides safe, testable service lifecycle hooks.
FreeBSD’s rc.d system does not maintain one giant list saying “start interface configuration, then routing, then SSH, then cron.” Each script declares ordering constraints, rcorder(8) converts those declarations into a sequence, and /etc/rc invokes scripts from that sequence under the policy in rc.conf(5). This distinction matters: ordering a script is not the same as enabling its daemon, and placing one script after another does not prove the earlier daemon is healthy or even running.
PROVIDE, REQUIRE, and BEFORE describe order
rcorder(8) reads specially formatted comments; it does not execute the file to discover dependencies:
#!/bin/sh
# PROVIDE: myservice
# REQUIRE: DAEMON NETWORKING
# BEFORE: LOGIN
# KEYWORD: shutdown
PROVIDE identifies conditions the script satisfies in the ordering graph. REQUIRE says that providers for the named conditions must appear earlier. BEFORE expresses the inverse constraint and is useful when the later script cannot be modified. A condition such as NETWORKING is a symbolic milestone, not necessarily a filename or proof that an external network is reachable.
The rcorder(8) manual is explicit about a frequently missed limitation: REQUIRE: sshd means “place this script after the script that provides sshd.” It does not mean that sshd is enabled, successfully started, or ready to accept connections. If a service truly needs a socket, file, directory, database response, or configuration value, its script or daemon must validate that runtime prerequisite separately.
Given all candidate scripts, rcorder constructs a directed dependency graph and emits a valid topological order. Missing providers and circular dependencies are errors, not timing quirks. Current FreeBSD can also produce Graphviz output for diagnosing loops and missing conditions. The basic non-executing check is:
rcorder /etc/rc.d/* /usr/local/etc/rc.d/*
This includes scripts regardless of whether their rcvar is enabled. /etc/rc later invokes them in order; run_rc_command normally declines a daemon’s start action unless its enable variable evaluates to YES. That two-stage model is why “it appears in rcorder” and “it starts at boot” answer different questions.
Keywords select boot, shutdown, and special sequences
KEYWORD labels let callers include or exclude subsets. FreeBSD uses keywords including firstboot, nojail, nojailvnet, nostart, suspend, resume, and shutdown. For example, rc.shutdown(8) selects scripts marked shutdown and processes the relevant ordering in reverse so dependents stop before the services they require.
The standard boot sequence skips scripts marked nostart; that keyword is for scripts which should be ordered but not automatically executed during normal startup. A useful inspection command is:
rcorder -s nostart /etc/rc.d/* /usr/local/etc/rc.d/*
To inspect only scripts carrying a particular keyword, use -k, not -s. For example, rcorder -k shutdown ... selects the shutdown set. Omitting KEYWORD: shutdown from a persistent third-party daemon means it is not part of the orderly shutdown pass. That may be correct for a one-shot configuration script, but it is usually a defect for a daemon that must flush durable state or close a journal.
Ordering remains declarative, not parallel process supervision. The rc framework invokes lifecycle commands and supplies common process checks; it does not continuously restart a crashed service, model readiness after fork(), or replace a dedicated supervisor. A daemon that exits after boot can remain down until monitoring or an administrator notices it.
A robust minimal service script
Port-installed scripts belong in /usr/local/etc/rc.d; base-system scripts live in /etc/rc.d. A conventional service script sources rc.subr(8), loads configuration, and lets run_rc_command implement the standard verbs:
#!/bin/sh
# PROVIDE: myservice
# REQUIRE: DAEMON NETWORKING
# KEYWORD: shutdown
. /etc/rc.subr
name="myservice"
rcvar="myservice_enable"
command="/usr/local/sbin/myservice"
command_args="-c /usr/local/etc/myservice.conf"
pidfile="/var/run/${name}.pid"
required_files="/usr/local/etc/myservice.conf"
load_rc_config "$name"
: ${myservice_enable:="NO"}
run_rc_command "$1"
load_rc_config reads the system configuration sources documented by rc.conf(5), including service-specific files. The default assignment keeps a missing setting disabled. rcvar connects the script to myservice_enable, while required_files prevents startup when a necessary file is absent. run_rc_command supplies actions such as start, stop, restart, status, poll, and rcvar when the declared variables support them.
Process identity requires care. A stale PID file is not evidence that the named daemon owns that PID, and a daemon which forks must write the PID that the script expects. Set pidfile, procname, and, for interpreted programs, command_interpreter according to the real process image. Test status, restart, and a crash-and-stale-PID scenario instead of assuming start success proves stop behavior.
rc.subr also supports required_dirs, required_vars, extra_commands, and pre/post hooks such as start_precmd and start_postcmd. Hooks return status, so a failing pre-command can abort startup cleanly. Use them for validation or preparation, not to hide a second daemon behind one service name. Custom actions must be listed in extra_commands, and action-specific functions use names such as reload_cmd.
Enablement and invocation are separate operations
Use sysrc(8) or the service(8) enable/disable verbs instead of editing rc.conf with fragile text substitution:
service myservice enable
service myservice start
service myservice status
service myservice rcvar
service myservice enable writes the relevant enable variable; start then honors it. onestart and onestop skip only the rcvar enablement check, which is useful for a deliberate one-time test:
service myservice onestart
service myservice status
service myservice onestop
The force prefix bypasses more safety checks than one and should not become a routine workaround. If start says a service is disabled, enabling it or using a one-time action is clearer than forcing past prerequisites.
service(8) also recreates the restricted boot environment: HOME=/ and PATH=/sbin:/bin:/usr/sbin:/usr/bin. This catches a class of scripts that work from an administrator’s interactive shell only because /usr/local/bin, aliases, or environment variables happen to be present. Scripts should use absolute paths for non-base commands and explicitly define any needed environment.
Useful inventory commands answer separate questions:
service -l # files in base and local rc directories
service -e # services whose rcvar is enabled
service -r # rcorder sequence, enabled or not
service -rv # verbose ordered inventory
Debugging an ordering or startup failure
First determine whether the error is graph construction, policy, or daemon behavior. Run rcorder and look for a missing provider or cycle. Then inspect service myservice rcvar and sysrc myservice_enable. Finally invoke service -d myservice onestart to enable rc debug output for a controlled manual test, and inspect system logs plus the daemon’s own log.
Do not “fix” an ordering issue by adding arbitrary sleeps. A sleep makes the race slower, not correct. If a daemon needs a local interface configuration milestone, express the appropriate REQUIRE. If it needs a remote database to accept queries, implement bounded readiness handling in the daemon or an explicit preflight with meaningful failure reporting. These are different dependencies.
For new scripts, test the complete lifecycle in an environment equivalent to boot: start disabled and verify normal start refuses; run onestart; check the real process and PID; reload if supported; stop; boot once; and perform an orderly shutdown. Validate both /etc/rc.d and /usr/local/etc/rc.d in rcorder, because a package update can introduce a provision conflict or cycle that an isolated script test misses.
The strength of rc.d is its modest contract. rcorder decides a consistent script sequence, rc.conf decides policy, rc.subr standardizes lifecycle mechanics, and the daemon remains responsible for its actual readiness and resilience. Most hard-to-diagnose rc failures come from treating one of those layers as though it guaranteed the others.
Related:
- Fixing Slow Boot Times Caused by Excessive rc.d Service Dependencies
- Inside the FreeBSD Boot Process: BIOS/UEFI, the Loader, and init
Sources: