Christian Lehnert — Linux, Hacking & Faith

FreeBSD's rc System

Christian Lehnert2026-06-20~11 min read

The Init System That Stayed Sensible

The Unix world has spent the last fifteen years arguing about init
systems. SysV gave way to upstart, upstart gave way to systemd, and
the resulting conversation produced more drama than enlightenment.
Through all of this, FreeBSD's rc system continued to do what it
had been doing since 2000: boot the machine using ordered shell
scripts, read configuration from a small set of well-defined files,
and stay out of the way.

The rc system is not glamorous. It does not have a journal. It
does not have a tree of namespaces. It does not have sandboxing
directives. What it has is clarity. The entire boot sequence on a
FreeBSD system can be read in a few hundred lines of shell, and a
sysadmin who has not touched a FreeBSD box in five years can sit
down at one and understand what is happening within minutes.

This post is the working reference for how the rc system actually
works. The path from /sbin/init to your own services running.
The configuration hierarchy that controls what gets started. The
script format that any service needs to follow. The
distinction between base-system services and ports services that
catches first-time FreeBSD operators. And the modern tooling
(sysrc, service) that makes the whole thing safe to manage.

The Boot Sequence

After the FreeBSD bootloader (loader.efi on UEFI, loader on
BIOS) has loaded the kernel and the kernel has finished its
initialization, control passes to /sbin/init, which becomes PID

  1. init reads /etc/ttys to determine what to do. In the
    default multi-user mode, it forks and executes /etc/rc. In
    single-user mode it spawns a root shell instead and skips /etc/rc
    entirely. The choice is made through the bootloader prompt or the
    -s flag at boot.
    /etc/rc is a shell script of approximately a hundred and seventy
    lines that orchestrates the rest of the boot. It performs three
    operations in sequence.

First, it parses options and sources /etc/rc.subr, which is the
shell library that provides every helper function the per-service
scripts will use. rc.subr is itself approximately a thousand
lines of shell and is the most important file in the rc system to
read once if you want to understand how anything works.

Second, it reads the configuration files in a specific order to
build the runtime environment. This order is the configuration
hierarchy described in the next section.

Third, it generates an ordered list of scripts to run by invoking
rcorder(8) against the contents of /etc/rc.d/ and
/usr/local/etc/rc.d/, and then loops through that list calling
each script with start as its argument.

When /etc/rc completes successfully, init resumes and handles
the per-terminal getty spawns declared in /etc/ttys, which is
how login prompts appear on consoles and serial lines. The system
is now in normal multi-user mode.

The Configuration Hierarchy

The configuration files that /etc/rc reads, in order, are:

/etc/defaults/rc.conf is the file shipped by FreeBSD base.
It contains every default value for every variable the rc system
knows about, with comments explaining each. This file should not
be edited. Its purpose is to be the single source of truth for
"what does FreeBSD do by default if I do not configure it
otherwise." It is approximately seven hundred lines long and is
worth reading once cover to cover. Every variable that controls
the boot is documented here.

/etc/rc.conf is the file the operator edits. It contains
overrides for the defaults. By convention this file holds only
the values that differ from the defaults, which keeps it small
and readable. A minimal FreeBSD server's /etc/rc.conf might
contain ten lines: hostname, network interface configuration,
SSH enabled, NTP enabled, syslogd flags, dump device, a couple of
service toggles. The defaults take care of everything else.

/etc/rc.conf.local is the historical file for site-specific
overrides that should not be confused with host-specific
configuration. Its semantic role is to layer above /etc/rc.conf
for shared site values when several hosts use a common
/etc/rc.conf. In modern practice this is rarely used because
configuration management tools (Ansible, Puppet, Salt, plain rsync
of a versioned /etc/rc.conf) handle the layering directly.

/etc/rc.conf.d/<service> is the per-service configuration
fragment directory, added later in FreeBSD's evolution. Each file
in this directory is sourced by the corresponding service's rc
script. This is the right place to put per-service tuning that
would otherwise clutter /etc/rc.conf. For example, putting
sshd_flags="-o ListenAddress=10.0.0.5" and similar SSH-specific
configuration in /etc/rc.conf.d/sshd keeps the main rc.conf
focused on the global toggles.

The composition rule is simple. Each file is sourced in order, and
later definitions override earlier ones. A variable set in
/etc/defaults/rc.conf is the floor. Any subsequent file can
raise or change it. By the time the per-service scripts run,
every variable has its final value.

The convention that every rc-aware service uses is
<servicename>_enable="YES" or ="NO". The base default for almost
every service is NO, which means a fresh FreeBSD system starts
no services beyond the absolute minimum. The operator opts in
explicitly to each service they want. This is the opposite of
some Linux distributions' default-on philosophy and is one of the
reasons FreeBSD systems are smaller and tighter than their Linux
equivalents at install time.

rcorder and Dependency Resolution

The dependency resolution that turns "a directory of scripts" into
"an ordered boot sequence" is handled by rcorder(8). The
mechanism is straightforward and avoids the runlevel numbering
that plagued SysV.

Each rc script declares its position in the boot sequence through
specially formatted comments near the top of the file:

1# PROVIDE: myservice
2# REQUIRE: NETWORKING DAEMON
3# BEFORE: LOGIN
4# KEYWORD: shutdown

PROVIDE declares what this script provides, which is normally
just the service name. REQUIRE lists services or virtual
targets that must have completed before this one runs. BEFORE
lists services or virtual targets that this script must complete
before. KEYWORD is metadata used to filter scripts in certain
contexts; shutdown means the script should also be called at
system shutdown, nojail means skip it inside a jail, firstboot
means run only on first boot.

The virtual targets like NETWORKING, DAEMON, LOGIN, and
FILESYSTEMS are documented in rc(8) and represent
synchronization points in the boot. NETWORKING means the
network interfaces are configured. DAEMON means daemons can
start. LOGIN means user logins can be accepted. A service that
requires the network specifies REQUIRE: NETWORKING and runs
after the network is up regardless of where it sits
alphabetically.

rcorder reads all the scripts, builds a directed graph from the
PROVIDE/REQUIRE/BEFORE declarations, performs a topological sort,
and outputs the ordered list. /etc/rc then iterates through
this list. The result is that the boot order is correct because
the dependency graph is correct, not because somebody numbered
the scripts.

The Anatomy of an rc Script

Every rc script follows the same skeleton. Understanding the
skeleton is sufficient to read or write any service script on a
FreeBSD system.

A minimal example for a custom service:

 1#!/bin/sh
 2 
 3# PROVIDE: myservice
 4# REQUIRE: NETWORKING DAEMON
 5# BEFORE: LOGIN
 6# KEYWORD: shutdown
 7 
 8. /etc/rc.subr
 9 
10name="myservice"
11desc="My Custom Service"
12rcvar="${name}_enable"
13 
14load_rc_config $name
15: ${myservice_enable:="NO"}
16: ${myservice_user:="myservice"}
17: ${myservice_flags:=""}
18 
19command="/usr/local/bin/myservice"
20command_args="-c /usr/local/etc/myservice.conf ${myservice_flags}"
21pidfile="/var/run/${name}.pid"
22 
23extra_commands="reload"
24 
25run_rc_command "$1"

The shebang and PROVIDE/REQUIRE comments are mandatory. The
script sources /etc/rc.subr to gain access to the helper
functions. It declares its name, description, and the rcvar
variable that controls whether it runs. It calls load_rc_config
to read its specific configuration values from the rc.conf
hierarchy. It declares the command to run, the arguments, and the
PID file location. It can declare extra commands beyond the
standard start/stop/restart. And it ends with run_rc_command "$1"
which is the dispatch function provided by rc.subr that handles
every standard operation.

The run_rc_command function provides start, stop, restart,
reload (if declared in extra_commands), status, poll, and others
out of the box. The script does not implement any of these
directly. It declares the metadata and lets rc.subr handle the
mechanics. This is why FreeBSD rc scripts are typically thirty
lines long while equivalent SysV init scripts were three hundred.
The shared library does the work.

Where Your Services Belong

A FreeBSD system has two directories for rc scripts, and the
distinction between them is the single most important convention
to understand.

/etc/rc.d/ holds scripts for services that are part of the
base system. sshd, syslogd, ntpd, pf, ipfw, cron, and
the rest of the base userland services live here. These scripts
are shipped and updated as part of FreeBSD itself. They should
not be modified by operators. freebsd-update will overwrite any
local changes.

/usr/local/etc/rc.d/ holds scripts for services installed
through ports or packages, plus any custom scripts the operator
writes. nginx, postgresql, redis, node_exporter, and every
third-party service installed through pkg ends up here. Custom
services you write for your own applications belong here too.

The separation between base and local is the same separation that
runs through all of FreeBSD: base lives under / (/bin,
/sbin, /etc, /usr/bin), local lives under /usr/local
(/usr/local/bin, /usr/local/sbin, /usr/local/etc,
/usr/local/etc/rc.d). This is the structural property that
makes freebsd-update safe: it touches only the base directories,
and your /usr/local is untouched across system updates.

When you write a custom service, the file goes in
/usr/local/etc/rc.d/. The script follows the skeleton above.
Its configuration variable defaults to NO. The operator opts in
via sysrc <name>_enable=YES (covered next). The first
service <name> start brings it up.

sysrc and service: The Modern Tooling

Two commands are worth committing to muscle memory because they
make the rc system safe to manage.

sysrc(8) is the right way to edit /etc/rc.conf. Manual
editing with vi works, but it is easy to introduce syntax errors
that prevent the rc system from loading the file correctly, which
breaks the boot in subtle ways. sysrc parses and rewrites
/etc/rc.conf atomically.

1sysrc sshd_enable=YES        # set or update a variable
2sysrc -x sshd_enable          # remove a variable
3sysrc -e                       # print all currently set variables
4sysrc -f /etc/rc.conf.d/sshd sshd_flags="-o ListenAddress=10.0.0.5"
5                               # edit a specific file

The -f flag is particularly useful for managing the
/etc/rc.conf.d/ fragments without manually navigating the
filesystem.

service(8) is the right way to control running services. It
dispatches to the appropriate rc script and provides several
convenience flags.

1service sshd start             # start a service
2service sshd status            # check status
3service sshd reload            # reload (if supported)
4service -e                     # list enabled services
5service -l                     # list all available scripts
6service sshd onestart          # start even if disabled in rc.conf

The one prefix is worth knowing. onestart, onestop, and
similar variants run the operation regardless of whether the
service is enabled in rc.conf. This is useful for ad-hoc service
control without permanently enabling a service in configuration.

Why This Has Aged Well

The reason the rc system has aged well, while every Linux init
system has been replaced at least once in the same period, is
that the rc system made a small number of correct design choices
in 2000 and then committed to them.

The configuration hierarchy is plain shell. There is no special
language. There is no parser to keep current. Any operator who
knows shell can read and modify it.

The dependency resolution is metadata-driven and topologically
sorted. There are no runlevel numbers to renumber when services
are added. There is no master list to keep in sync. Each script
declares its own requirements and the system computes the order
automatically.

The script format is short because the work is in rc.subr. A
hundred-line rc.subr improvement benefits every script on the
system. Per-script logic is just declarations.

The base-local split is structural. Operators cannot accidentally
edit base scripts because they live in a different directory than
local scripts. freebsd-update knows the same boundary and
respects it.

The whole system is approximately three thousand lines of shell.
Operators who want to understand exactly what is happening during
boot can read every line of it in an afternoon. The boundary
between "operator-readable" and "magic" is the same boundary as
"is this shell script?" There is no magic.

Closing

For operators coming to FreeBSD from systemd-managed Linux, the
rc system can feel sparse. There is no journalctl for service
logs (services log to /var/log files via syslogd). There is
no socket activation. There are no cgroup directives or
sandboxing flags. The mental model of "service definition lives in
a declarative file that the init system interprets" is replaced
by "service definition is a small shell script that calls a
shared library."

This is not a regression. It is a different set of trade-offs.
The rc system optimizes for legibility, longevity, and the
absence of cross-coupling between init concerns and other
subsystems. systemd optimizes for tight integration with
namespacing, cgroups, and modern Linux kernel features. Both are
defensible choices for their respective systems.

If you operate FreeBSD, learn rc.subr. Read /etc/defaults/rc.conf
once. Use sysrc to edit configuration. Use service to
control services. Put your own scripts in
/usr/local/etc/rc.d/. The system will do what you tell it to
do, in the order you tell it to, every time, for as long as you
operate it. That is the value the rc system provides, and it has
been providing it consistently for a quarter century.

Tagged:
#freebsd #rc #init #services
← Back to posts