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.
 
Back
Top