How rc Actually Works at Boot
When FreeBSD finishes loading the kernel, init(8) runs /etc/rc. That script sources /etc/rc.subr, reads /etc/defaults/rc.conf first, then overlays your customizations from /etc/rc.conf. The result is a flat key-value environment used to decide which rc.d scripts to run and in what order.
Order is determined by REQUIRE, PROVIDE, BEFORE, and KEYWORD lines inside each rc.d script. The rcorder(8) utility reads those metadata lines and emits a sorted list of scripts. You can inspect the resolved boot order yourself without rebooting:
This gives you the full dependency-resolved sequence, one script path per line. On a minimal FreeBSD 14.1 jail we counted 43 scripts in the output. A full desktop install with ports will push that past 90.
rcorder /etc/rc.d/* /usr/local/etc/rc.d/*
rc.conf: The Single Source of Truth
/etc/rc.conf is where you enable or disable services and set their parameters. The file is sourced as shell, so every line is a variable assignment. /etc/defaults/rc.conf defines defaults for every base system service. Your /etc/rc.conf only needs to override what differs from those defaults.
A common mistake from Linux admins is editing /etc/defaults/rc.conf. Do not. It gets overwritten by freebsd-update(8). Your /etc/rc.conf survives updates.
To enable nginx installed from ports, add one line. That is all rc needs to pick it up on next boot. The _enable suffix is mandatory. Without it, the script skips activation even if the binary is present.
For parameters, the naming convention is servicename_flags or servicename_optionname. Check the rc.d script itself to see what variables it reads - they are always documented in the script header or set via load_rc_config.
# /etc/rc.conf
nginx_enable="YES"
nginx_flags="-c /usr/local/etc/nginx/nginx.conf"
sshd_enable="YES"
sshd_flags="-o UseDNS=no"
# PostgreSQL 16 from ports
postgresql_enable="YES"
postgresql_data="/var/db/postgres/data16"
postgresql_flags="-s"
service(8): Runtime Control Without Rebooting
service(8) is the runtime interface to rc scripts. It is the FreeBSD equivalent of systemctl for the start/stop/restart/status subset. service reads rc.conf, loads the appropriate rc.d script, and calls the action you specify.
The onestart and onestop variants bypass the _enable check. Use them when you want to start something temporarily without permanently enabling it - useful during testing or incident response.
service -e lists all currently enabled services. service -l lists all available service scripts. Both are useful for auditing a server you have inherited.
Status output is minimal by default. For nginx you get a PID. For more verbose state, many scripts implement a configtest or status action with extra detail - check the script to know what is available.
# Standard controls
service nginx start
service nginx stop
service nginx restart
service nginx status
service nginx reload
# Start without enabling in rc.conf
service nginx onestart
# Audit enabled services
service -e
# List all available scripts
service -l
Writing a Custom rc.d Script
The rc.subr framework gives you a structured way to write service scripts that integrate with rcorder, service(8), and rc.conf. A minimal script for a custom Go binary or Python daemon follows a fixed pattern.
The PROVIDE line declares what this script offers to the dependency graph. REQUIRE lists what must already be running. The most common requirements are NETWORKING (network stack up) and LOGIN (multi-user mode reached). BEFORE lets you insert before something else in the graph.
Place your script in /usr/local/etc/rc.d/ and make it executable with chmod 555. The 555 permission is conventional for rc.d scripts - owner read/execute, group and world read/execute. Scripts in /etc/rc.d/ are base system and should not be edited.
The command variable points to the binary. command_args passes fixed arguments. procname is used for PID detection when the binary name differs from the script name. pidfile tells rc where to look for the PID so stop and status work correctly.
After dropping the script, run rcorder to verify it lands in the expected position in the boot sequence, then enable it in rc.conf and test with service yourapp start.
#!/bin/sh
#
# PROVIDE: yourapp
# REQUIRE: NETWORKING postgresql
# BEFORE: LOGIN
# KEYWORD: shutdown
. /etc/rc.subr
name="yourapp"
rcvar="yourapp_enable"
load_rc_config $name
: ${yourapp_enable:="NO"}
: ${yourapp_user:="www"}
: ${yourapp_dir:="/opt/yourapp"}
: ${yourapp_flags:=""}
command="/opt/yourapp/bin/yourapp"
command_args="--config /opt/yourapp/etc/app.conf ${yourapp_flags}"
pidfile="/var/run/${name}.pid"
required_files="/opt/yourapp/etc/app.conf"
start_precmd="yourapp_prestart"
yourapp_prestart()
{
if [ ! -d /var/run/yourapp ]; then
install -d -o ${yourapp_user} -m 750 /var/run/yourapp
fi
}
run_rc_command "$1"
Dependency Ordering: REQUIRE, PROVIDE, BEFORE
rcorder resolves dependencies at boot by reading metadata comments at the top of each script. These are not shell variables - they are parsed as comments with a specific format. Getting them wrong causes silent boot failures where services start before their dependencies are ready.
PROVIDE declares a capability name. Multiple scripts can PROVIDE the same name, which is how you swap implementations. For example, both /etc/rc.d/sendmail and a ports-installed postfix can PROVIDE mail_mta, and rcorder will include whichever is enabled.
REQUIRE lists capabilities that must have run first. Use the abstract names (NETWORKING, FILESYSTEMS, LOGIN) rather than script names where possible, because port upgrades can rename scripts.
BEFORE is the reverse: this script must run before the named capability. Use it sparingly. Circular dependencies cause rcorder to fail silently by dropping scripts from the boot sequence.
KEYWORD: shutdown tells the system to run this script's stop action during shutdown. Without it, your daemon gets SIGTERM from the kernel directly during shutdown without a clean stop. Always include it for daemons that write data or hold file locks.
# Inspect what a script provides and requires
grep -E '^# (PROVIDE|REQUIRE|BEFORE|KEYWORD)' /usr/local/etc/rc.d/postgresql
# Verify your script is in the correct order
rcorder /etc/rc.d/* /usr/local/etc/rc.d/* | grep -n yourapp
# Check what comes immediately before and after
rcorder /etc/rc.d/* /usr/local/etc/rc.d/* | \
grep -n -A2 -B2 yourapp
rc.conf.d: Per-Service Configuration Files
FreeBSD 10.0 added /etc/rc.conf.d/ as a directory for per-service configuration fragments. Instead of one monolithic /etc/rc.conf, you can split configuration into individual files named after the service. The rc system loads /etc/rc.conf.d/nginx automatically when processing the nginx rc.d script.
This is particularly useful in infrastructure-as-code workflows where you generate service configs from templates. Each service gets its own file, making diffs readable and reducing merge conflicts in version control. Teams using tools like Ansible or Salt drop rendered templates directly into /etc/rc.conf.d/.
The file naming matches the service name exactly. A file for sshd goes at /etc/rc.conf.d/sshd. The content is identical in syntax to rc.conf.
# /etc/rc.conf.d/postgresql
postgresql_enable="YES"
postgresql_data="/var/db/postgres/data16"
postgresql_class="postgres"
postgresql_initdb_flags="--encoding=utf-8 --lc-collate=C"
# /etc/rc.conf.d/nginx
nginx_enable="YES"
nginx_flags=""
# Verify the fragment is being loaded
sh -x /etc/rc.d/postgresql start 2>&1 | head -30
Jail-Aware Services and rc.conf
FreeBSD jails have their own rc system. Each jail runs its own /etc/rc at start, reading its own /etc/rc.conf. The host's rc.conf controls jail startup via the jail_enable and jail_list variables, but once a jail boots, it manages its services independently.
On the host, jail(8) is itself an rc.d script at /etc/rc.d/jail. It reads /etc/jail.conf or /etc/jail.conf.d/*.conf to get jail parameters. Services inside a jail are configured identically to the host - same rc.conf syntax, same service(8) commands, same /usr/local/etc/rc.d/ drop-in location.
When running service commands from the host into a jail, use jexec. This is preferable to sshing into the jail for administrative tasks because it does not require sshd running inside the jail.
# Host rc.conf for jail management
jail_enable="YES"
jail_list="db01 web01 cache01"
# Start a specific jail
service jail start web01
# Control a service inside a running jail from the host
jexec web01 service nginx restart
jexec web01 service nginx status
# Check all enabled services inside a jail
jexec db01 service -e
Debugging Boot Failures
When a service fails to start at boot, the first tool is /var/log/messages. FreeBSD rc writes service start/stop events there. For a service that dies immediately after starting, the log typically shows the exit code.
For deeper debugging, you can run the rc.d script with sh -x to trace every line. This is faster than adding echo statements and works on scripts you did not write.
The rcorder -s and -k flags filter scripts by KEYWORD. If you are debugging a specific shutdown sequence issue, rcorder -k shutdown shows only scripts that run during shutdown.
For services that depend on timing - waiting for a network interface, a mount point, or another daemon to open a socket - the rc.subr framework provides wait_for_pids and checkyesno helper functions. A common pattern for services with slow startup is setting the start_postcmd to poll for a socket or PID file rather than trusting the daemon's own daemonize behavior.
If you manage many FreeBSD servers or are building automated deployment pipelines, tools like taskbotshub.ai can help orchestrate multi-host rc service state checks, especially when validating that services came up correctly across a fleet after a coordinated reboot or upgrade.
# Trace rc.d script execution
sh -x /usr/local/etc/rc.d/yourapp start
# Check recent service events
grep 'yourapp' /var/log/messages | tail -20
# Test rc.conf syntax before rebooting
sh -n /etc/rc.conf && echo 'syntax OK'
# Dry-run rcorder to catch circular dependency errors
rcorder /etc/rc.d/* /usr/local/etc/rc.d/* 2>&1 | grep -i 'cycle\|error'
# Check what rc loaded for a specific service
sysrc -a | grep nginx
sysrc: Scripting rc.conf Changes Safely
sysrc(8) is the correct tool for scripting changes to rc.conf. It performs atomic reads and writes, handles quoting correctly, and supports /etc/rc.conf.d/ fragments. Using sed or awk on rc.conf directly is fragile and breaks with unusual quoting.
sysrc -a dumps all currently set rc.conf variables across all sources. sysrc -f /path/to/file targets a specific file, which is useful when managing jail configs or rc.conf.d fragments. The -n flag prints only the value without the variable name, making it suitable for use in scripts.
In Ansible roles for FreeBSD, the community.general.sysrc module wraps this command. If you are writing shell-based provisioning, call sysrc directly rather than templating the entire rc.conf file.
# Enable a service
sysrc nginx_enable=YES
# Disable a service
sysrc nginx_enable=NO
# Set a value
sysrc postgresql_data=/var/db/postgres/data16
# Read a value (returns 'YES' or 'NO')
sysrc -n nginx_enable
# Delete a variable from rc.conf
sysrc -x nginx_flags
# Target a specific rc.conf.d fragment
sysrc -f /etc/rc.conf.d/sshd sshd_flags="-o MaxSessions=20"
# Dump all active rc.conf settings
sysrc -a
Migrating from systemd Mental Models
The most common confusion for Linux engineers is that FreeBSD rc has no native equivalent of systemd socket activation or service restarts on failure. If you need a daemon restarted on crash, you have two options: write a wrapper rc.d script that loops with a sleep, or use a supervisor like daemon(8) with its -r flag for restart on exit.
daemon(8) is a base system utility that wraps any process, provides PID file management, stdout/stderr redirection, and restart behavior. It is the simplest path for keeping a service running without importing a full supervisor like supervisor or s6.
For log management, FreeBSD base includes newsyslog(8) for rotation. There is no journald. Daemons write to syslog or directly to files. If you are accustomed to journalctl -f for following service output, the equivalent for a daemon managed via daemon(8) is tail -f on whatever log file you pointed -o at.
For teams running automated infrastructure and wanting to track service health metrics or trigger remediation workflows across FreeBSD hosts, taskbotshub.ai provides integrations that can poll service status via jexec or SSH and trigger runbooks - a useful pattern when you have dozens of jails each with their own rc state.
# Run a binary under daemon(8) with restart and logging
daemon -r -P /var/run/myapp.pid \
-o /var/log/myapp.log \
-u www \
/opt/myapp/bin/myapp --config /opt/myapp/etc/config.toml
# In an rc.d script, use daemon via command_interpreter
# or call it directly in start_cmd:
start_cmd="daemon -r -P ${pidfile} -o /var/log/${name}.log \
-u ${yourapp_user} \
${command} ${command_args}"
Practical Patterns for Production FreeBSD Servers
On a production FreeBSD 14.x server running multiple jails, a clean rc.conf.d layout by service makes configuration management tractable. Keep /etc/rc.conf minimal - just jail_enable, hostname, and network interface config. Move everything else into /etc/rc.conf.d/. This way, a service outage caused by a misconfiguration is isolated to one file and one service.
For new projects where you are naming a service or daemon that will run long-term, think carefully about the PROVIDE name you choose - it becomes part of your dependency graph and changing it later requires updating all REQUIRE lines in dependent scripts. The same deliberate naming discipline applies if you are registering a domain or brand for a tool you are building. Services like nicename.me make it straightforward to check domain availability alongside brand name research, which is worth doing early before you embed a name into infrastructure.
Version-pin your port-installed services in /etc/rc.conf.d/ by keeping the data directory path explicit. PostgreSQL is the most common example: when you upgrade from pg16 to pg17, the data directory changes. Having postgresql_data set explicitly in /etc/rc.conf.d/postgresql means the old value is visible and intentional, rather than relying on the script default.
For firewall and network services, order matters critically. pf, ipfw, and ipfilter all PROVIDE firewall. If you enable multiple firewall rc.d scripts, only the last one to run wins. Audit with rcorder and confirm only one firewall script is enabled via sysrc -a | grep enable | grep YES.
# Recommended /etc rc.conf.d layout
ls /etc/rc.conf.d/
# hostname jail network pf sshd
ls /usr/local/etc/rc.conf.d/
# nginx postgresql redis unbound
# /etc/rc.conf - keep it minimal
cat /etc/rc.conf
hostname="db01.example.com"
ifconfig_em0="inet 10.0.1.10 netmask 255.255.255.0"
defaultrouter="10.0.1.1"
jail_enable="YES"