Running a Service on FreeBSD: the rc.d scripting

The BSD Daemon

Coming from Linux, I thought installing and running a service/daemon on FreeBSD was somewhat unintuitive, so I am writing a note.

I had been using FreeBSD 13.0 running on RPi 4 as a router to my home network since I set it up early this year. FreeBSD had been a solid platform. It was interesting and not-too-frustrating to set up the firewall, dhcpd, and DNS server on FreeBSD because they are well integrated with the system.

I learned how to run programs in isolation with FreeBSD Jail and iocage, which are the FreeBSD analogs of Docker containers in Linux. I had my simple service running in a Jail, but had been defering configuring it as a system service because it involved learning about the rc.d scripts.

The rc.d scripts are a part of the FreeBSD init system. An rc.d script corresponds to a systemd unit file, but the facilities are more basic. I wanted to turn my executable that listens on port and outputs log as stderr into a service. On Linux, I'd throw down the simplest systemd unit file like this one and call it done:

/etc/systemd/system/gemini-server.service

[Unit]
Description=gemini server
After=network.target

[Service]
ExecStart=/app/server \
	-listen-addr=0.0.0.0:1965 \
	-content-dir=/app/public
Restart=no

[Install]
WantedBy=multi-user.target

There was a helpful guide Practical rc.d scripting in BSD that covers writing an rc.d script. But it didn't cover two facilities that I needed: daemonizing and redirecting output to syslog. This had me puzzled when testing the script because these two facilities are provided implicitly when writing a systemd service.

Peeking at other scripts made me realize that /usr/sbin/daemon command was what I needed to daemonize the executable. Conveniently, It also had options to redirect output to syslog. I got the working rc.d script:

/usr/local/etc/rc.d/geminiserver

#!/bin/sh

. /etc/rc.subr

name="geminiserver"
rcvar="geminiserver_enable"

load_rc_config ${name}

: ${geminiserver_options="-listen-addr=0.0.0.0:1965 -content-dir=/app/public"}

pidfile="/var/run/geminiserver.pid"
procname="/app/server"
command="/usr/sbin/daemon"
command_args="-c -S -T ${name} -p ${pidfile} ${procname} ${geminiserver_options}"

run_rc_command "$1"

Bonus: logs are not very useful until they are shipped to somewhere I can process and browse them. When I tried to configure the log collector, I was stopped by the log format that syslogd produced by default:

May  1 08:15:32 www1 geminiserver[12159]: {"level":"info","msg":"Hello!","time":"2022-05-01T08:15:32Z"

I quickly learned that there are two syslog formats in use, RFC3164 (old) and RFC5424, but this did not fit exactly either of those. But it could be persuaded into one with the flag -o rfc5424.

sysrc syslogd_flags="-c -ss -O rfc5424"
service syslogd reload

A little daemon.

  • https://nostarch.com/pf3 "Book of PF, 3rd Edition"
  • https://mwl.io/nonfiction/os#fmjail "FreeBSD Mastery: Jails"