A rc Script For Elixir On FreeBSD - 2023-06-13
The issue
When I deployed my first elixir based application to a FreeBSD jail I had some issues stopping the service. Starting worked like a charm, but whenever I tried to stop it, the following error appeared:
# service app stop
app not running? (check /var/run/app/app.pid).
This was confusing to me, because the process identifier (PID) in the mentioned file was correct and when I tried to start the process again (while it was already running) I received this error:
# service app start
Starting app.
daemon: process already running, pid: 74473
/usr/local/etc/rc.d/app: WARNING: failed to start app
Something was off and after reading some documentation I found out what it was.
Background
Starting an elixir application is usually done by executing a script. I am using
the Phoenix Framework which creates this
script using the mix phx.gen.release
command. The way I designed the deployment, the generated script eventually
lands at the following location on a FreeBSD host:
/usr/local/app/v1.2.3/bin/server
.
When executed, the script will start the erlang virtual machine (BEAM) with a very long command. It might look something like this:
/usr/local/app/v1.2.3/erts-13.2/bin/beam.smp -- -root /usr/local/app/v1.2.3 -bindir /usr/local/app/v1.2.3/erts-13.2/bin -progname erl -- -home /home/app -- -noshell -s elixir start_cli -mode embedded -setcookie I_AM_A_COOKIE -sname app -config /usr/local/app/v1.2.3/releases/v1.2.3/sys -boot /usr/local/app/v1.2.3/releases/v1.2.3/start -boot_var RELEASE_LIB /usr/local/app/v1.2.3/lib -- -extra --no-halt
Somehow we need to make the rc script aware of this long command. Or at least parts of it.
The solution
Assuming our service is called app
, we will place the following rc script at
/usr/local/etc/rc.d/app
.
#!/bin/sh
# PROVIDE: app
# REQUIRE: LOGIN DAEMON NETWORKING
# KEYWORD: shutdown
. /etc/rc.subr
name="app"
rcvar="app_enable"
command="/usr/sbin/daemon"
pidfile="/var/run/app/${name}.pid"
task="/usr/local/app/v1.2.3/bin/server"
procname="*beam.smp*"
app_chdir="/usr/local/app"
command_args="-p ${pidfile} -t ${name} -u app -o /var/log/app.log ${task}"
load_rc_config $name
run_rc_command "$1"
To get a general understanding of rc files I can recommend the “Practical rc.d scripting in BSD”-Guide.
The important part for an elixir application and this scenario is the
combination of command
, command_args
, task
and procname
.
command
defines what is executed on startcommand_args
is passed to the command as argumenttask
only exists to enhance readability ofcommand_args
procname
is by default set to the same value ascommand
. It must contain the process name at runtime (that is: after it is started).
You might already guess the problem: command
contains the script which is created by
mix phx.gen.release
. And procname
must contain the command which is
eventually created by the script in command
.
When you do not set procname
to e.g. *beam.smp*
you can still run service app start
to start the service, but running service app stop
will fail because it
will check two things:
- find the process matching the PID specified in the
pidfile
- check if the process name matches whatever is specified in
procname
The first check will succeed because the PID exists. But the second check will
fail because the process name generated to run the BEAM is different from the
value in command
and - if not set - procname
.
To solve this issue, I set procname
to a globbed string containing beam.smp
which works fine. It might become an issue when you have multiple BEAM processes
fighting for PIDs.
Should this becomes an issue, try to make the procname
more restrictive like
e.g.: /usr/local/app/v1.2.3/erts-13.2/bin/beam.smp*
.
Finally, to enable this service, add the following line to /etc/rc.conf
:
app_enable="YES"
Now you can start and stop your elixir application using the usual service app <start|stop|restart>
commands.