writing a service script for www/radicale

In a development environment I installed radicale, a CalDAV/CardDAV server. I know I can install radicale via pkg or from ports, but this one I installed locally in a user account, /home/testuser/.local/bin/radicale (I did so by uv tool install radicale). Then, with the help of the grok AI, I wrote this script to start radicale as a service, this is in /usr/local/etc/rc.d/radicale:

sh:
#!/bin/sh

# PROVIDE: radicale
# REQUIRE: DAEMON NETWORKING
# KEYWORD: shutdown

. /etc/rc.subr

name="radicale"
rcvar="radicale_enable"

extra_commands="configtest reload"

load_rc_config $name

: ${radicale_enable:="NO"}
: ${radicale_user:="radicale"}
: ${radicale_group:="radicale"}
: ${radicale_command:="/home/testuser/.local/bin/radicale"}
: ${radicale_config:="/usr/local/etc/radicale/config"}

# Logging configuration
: ${radicale_syslog_tag:="radicale"}
: ${radicale_syslog_facility:="daemon"}
: ${radicale_syslog_priority:="info"}

# Extra flags for Radicale (e.g. --debug, -D, etc.)
: ${radicale_flags:=""}

pidfile="/var/run/radicale/${name}.pid"

command="/usr/sbin/daemon"

command_args="-f -P ${pidfile} \
      -u ${radicale_user} \
      -l ${radicale_syslog_facility} \
      -s ${radicale_syslog_priority} \
      -T ${radicale_syslog_tag} \
      ${radicale_command} \
      ${radicale_flags} \
      -C ${radicale_config}"

# Prepare pid directory before starting
start_precmd="radicale_precmd"
radicale_precmd() {
    install -d -o ${radicale_user} -g ${radicale_group} -m 700 /var/run/radicale
}

# Custom commands

radicale_configtest() {
    echo "Performing config test for Radicale..."

    if [ ! -f "${radicale_config}" ]; then
        echo "ERROR: Config file not found: ${radicale_config}"
        return 1
    fi

    if [ ! -r "${radicale_config}" ]; then
        echo "ERROR: Config file is not readable: ${radicale_config}"
        return 1
    fi

    echo "Config file found and readable: ${radicale_config}"

    echo "Running basic verification..."

    if su -m "${radicale_user}" -c "${radicale_command} --verify-storage -C ${radicale_config}" 2>&1 | head -n 30; then
        echo "Storage verification passed."
        return 0
    else
        echo "Storage verification completed (check output above for warnings)."
        return 0
    fi

    echo "Config test completed."
}

radicale_reload() {
    echo "Reloading Radicale configuration..."
    if [ -f "${pidfile}" ]; then
        kill -HUP $(cat "${pidfile}")
        echo "Sent SIGHUP to Radicale."
    else
        echo "Radicale is not running (no pidfile)."
        return 1
    fi
}

oneconfigtest_cmd="radicale_configtest"
configtest_cmd="radicale_configtest"
reload_cmd="radicale_reload"

run_rc_command "$1"

I created the according files and users:
Code:
# getent passwd radicale
radicale:*:1003:1006:Radicale CalDAV server:/var/lib/radicale:/usr/sbin/nologin

# getent group radicale
radicale:*:1006

# service radicale oneconfigtest
Performing config test for Radicale...
Config file found and readable: /usr/local/etc/radicale/config
Running basic verification...
[2026-05-16 10:37:38 +0200] [41412] [INFO] Logging level set to: 'INFO'
[2026-05-16 10:37:38 +0200] [41412] [INFO] Logging of backtrace is disabled in this loglevel
[2026-05-16 10:37:38 +0200] [41412] [INFO] Logging level set to: 'INFO'
[2026-05-16 10:37:38 +0200] [41412] [INFO] Loaded default config
[2026-05-16 10:37:38 +0200] [41412] [INFO] Loaded config file '/usr/local/etc/radicale/config'
[2026-05-16 10:37:38 +0200] [41412] [INFO] Verifying storage
[2026-05-16 10:37:38 +0200] [41412] [INFO] storage type is 'radicale.storage.multifilesystem'
[2026-05-16 10:37:38 +0200] [41412] [INFO] Storage folder umask (from system): '0022'
[2026-05-16 10:37:38 +0200] [41412] [INFO] Storage location: '/var/lib/radicale/collections'
[2026-05-16 10:37:38 +0200] [41412] [INFO] Storage location permissions: path='/var/lib/radicale/collections' owner=radicale(1003) group=radicale(1006) mode=40700
[2026-05-16 10:37:38 +0200] [41412] [INFO] Storage location subfolder: '/var/lib/radicale/collections/collection-root'
[2026-05-16 10:37:38 +0200] [41412] [INFO] Storage location subfolder permissions: path='/var/lib/radicale/collections/collection-root' owner=radicale(1003) group=radicale(1006) mode=40755
[2026-05-16 10:37:38 +0200] [41412] [INFO] Storage location subfolder softlink support: True
[2026-05-16 10:37:38 +0200] [41412] [INFO] Storage location subfolder is collision free: True (case-sensitive=True no-short-filename=True)
[2026-05-16 10:37:38 +0200] [41412] [INFO] Storage location subfolder supports unicode: True
[2026-05-16 10:37:38 +0200] [41412] [INFO] Storage location subfolder supports trailing whitespace: True
[2026-05-16 10:37:38 +0200] [41412] [INFO] Storage location subfolder supports problematic chars: True
[2026-05-16 10:37:38 +0200] [41412] [INFO] Storage cache subfolder usage for 'item': False
[2026-05-16 10:37:38 +0200] [41412] [INFO] Storage cache subfolder usage for 'history': False
[2026-05-16 10:37:38 +0200] [41412] [INFO] Storage cache subfolder usage for 'sync-token': False
[2026-05-16 10:37:38 +0200] [41412] [INFO] Storage cache use mtime and size for 'item': False
[2026-05-16 10:37:38 +0200] [41412] [INFO] Storage item mtime resolution test result: 1 ns
[2026-05-16 10:37:38 +0200] [41412] [INFO] Storage cache using mtime and size for 'item' may be an option in case of performance issues
[2026-05-16 10:37:38 +0200] [41412] [INFO] Disable fsync during storage verification
[2026-05-16 10:37:38 +0200] [41412] [INFO] Verifying   path ''
[2026-05-16 10:37:38 +0200] [41412] [INFO] Skip !collection ''
Storage verification passed.

So everything looks ok, but the service fails to start because it lacks permissions when executing initgroups():
Code:
# service radicale start
Starting radicale.

# service radicale status
radicale is not running.

# tail /var/log/messages | fgrep radicale
May 16 10:50:58 bedna radicale[41618]: initgroups(radicale,1006): Operation not permitted

And here I'm stuck, and so is grok. It works if I modify the rc script to run the radical service as the root user (just omitting the -u parameter of daemon():


sh:
# ...
command_args="-f -P ${pidfile} \
      -l ${radicale_syslog_facility} \
      -s ${radicale_syslog_priority} \
      -T ${radicale_syslog_tag} \
      ${radicale_command} \
      ${radicale_flags} \
      -C ${radicale_config}"
# ...

but I really don't like having a service process run as root. So, how could I solve this? Why does initgroups fail?
 
Interesting. I removed the -u option from command_args as I described in my original post and after starting the service I see that it is actually not running as root:


Code:
❯ ps -U radicale
  PID TT  STAT    TIME COMMAND
42543  -  Is   0:00.00 daemon: /home/tejul/.local/bin/radicale[42544] (daemon)
42544  -  I    0:00.34 /home/testuser/.local/share/uv/tools/radicale/bin/python3 /home/testuser/.local/bin/radicale -C /usr/local/etc/radicale/config (python3.14)

why is that?
 
why is that?

The 'default' start() function already has something for this. You defined radicale_user, thus it's going to be started on that user with su(1).

Code:
# ${name}_user  n User to run ${command} as, using su(1) if not
#       using ${name}_chroot.
#       Requires /usr to be mounted.
Just have a look at /etc/rc.subr which has all the functions the rc.d(8) scripts use.
 
I have a similar problem. Here's an erlang service name 'example':

Code:
example 30901   4.6  1.4 1323088 59744  -  S    07:32        0:00.36 /usr/local/lib/erlang28/erts-16.4/bin/beam.smp -- -root /usr/local/lib/erlang28 -bindir /usr/local/lib/erlang28/erts-16.4/bin -progname erl -- -home /nonexistent -- -pa /usr/local/libexec/example/envoy/ebin /usr/local/libexec/example/example/ebin /usr/local/libexec/example

The problem is that the procname is `/usr/local/lib/erlang28/erts-16.4/bin/beam.smp`. But it's actually an interpreter (or runtime).

However, the version of the interpreter can change separate from the service.

If I hardcode the `procname` in the service's rc script to be `/usr/local/lib/erlang28/erts-16.4/bin/beam.smp`, then everything works fine...until the interpreter/runtime version changes. Even a minor change in the interpreter/runtime will cause commands such as `service example status` to fail, e.g. it will show the service as not started when it is running. While the erlang version doesn't change that often (e.g. erlang28) the version of erts changes more frequently (in my experience anyway), and that's what has bitten me today.

To recap:

/etc/rc.d/example
Code:
# procname="beam.smp" // I guess I misread the docs, but I thought this might work, but it does NOT work
procname="/usr/local/lib/erlang28/erts-16.4/bin/beam.smp" // this works so long as this version matches what's on disk
interpreter="." // the presence or absence of this makes no difference in my tests

I _could_ look at the filesystem when I build my service package and hardcode the procname using the current erlang version. That's what patmaddox is doing in ex_freebsd. However, I don't want my services to stop starting/stopping correctly just because my beam has changed from /usr/local/lib/erlang28/erts-16.2/bin/beam.smp to /usr/local/lib/erlang28/erts-16.4/bin/beam.smp.

So, is there a way to tell /etc/rc.subr that I want to use `/usr/local/lib/erlangf28/*/beam.smp` as my procname? Or anything similar?
 
Um. Doh. This actually works just the way I want it to:

/etc/rc.d/example
Code:
procname="/usr/local/lib/erlang28/*/bin/beam.smp"

Service start and stop work as expected.
 
Back
Top