Autostart rails + rvm + unicorn + nginx on FreeBSD

First, thank you to the author of this guide . Without it, I would not have sat down for a long time writing this post: a lot of problems would have to be solved independently. However, in my case, the situation was slightly different (not Debian, but FreeBSD), and the issue of unicorn autostart remained open. The solutions to elegance that I met on the Internet did not pretend either: to do a job on a web application - Moveton. In FreeBSD, this problem is solved at first glance simply by creating "meta-services" that allow you to run more than one instance (as an example, FreeBSD jails). However, as often happens, there are nuances ...

rc.d

Since all rc.d-configs are shell scripts, you can not be limited to the file /etc/rc.confand its counterpart /etc/rc.conf.local, but to place fragments in the /etc/rc.conf.dand directories /usr/local/etc/rc.conf.d. The only recommendation is that the files should be named in the same way as the services using them. This allows you to avoid turning configuration files into unreadable monsters of more than a hundred lines, which you can’t figure out without grep. The problem is different: if the service has several instances, ALL of their settings should formally be in the same file (whose name, I recall, coincides with the name of the service). To solve this problem, you need to understand how the scripts for launching services work.
Typically, this process is described as follows: "When the system starts, a script is executed /etc/rc. It reads the settings from the file/etc/rc.conf(and also /etc/rc.conf.local, if there is one) ... "And here it is! Instead, it includes a file /etc/rc.subrthat defines some auxiliary functions and, in the same way, includes system settings files:
...
if ${_rc_conf_loaded:-false}; then
        :
else
        if [ -r /etc/defaults/rc.conf ]; then
                debug "Sourcing /etc/defaults/rc.conf"
                . /etc/defaults/rc.conf
                source_rc_confs
        elif [ -r /etc/rc.conf ]; then
                debug "Sourcing /etc/rc.conf (/etc/defaults/rc.conf doesn't exist)."
                . /etc/rc.conf
        fi
        _rc_conf_loaded=true
fi
if [ -f /etc/rc.conf.d/"$_name" ]; then
        debug "Sourcing /etc/rc.conf.d/${_name}"
        . /etc/rc.conf.d/"$_name"
fi
...

In turn, the file /etc/defaults/rc.confcontains the following code:
...
rc_conf_files="/etc/rc.conf /etc/rc.conf.local"
...
##############################################################
### Define source_rc_confs, the mechanism used by /etc/rc.* ##
### scripts to source rc_conf_files overrides safely.       ##
##############################################################
if [ -z "${source_rc_confs_defined}" ]; then
        source_rc_confs_defined=yes
        source_rc_confs () {
                local i sourced_files
                for i in ${rc_conf_files}; do
                        case ${sourced_files} in
                        *:$i:*)
                                ;;
                        *)
                                sourced_files="${sourced_files}:$i:"
                                if [ -r $i ]; then
                                        . $i
                                fi
                                ;;
                        esac
                done
        }
fi

After loading all the settings, a list of system services is compiled, and a startup script is called for each service - regardless of whether it is enabled or not. Therefore, it is strongly discouraged to place user service files in a directory/etc/rc.d - this will increase the system boot time. There is a directory for user services /usr/local/etc/rc.d- the scripts located in it are called last, when the main system is already configured and ready to work. But the real black magic begins further: each of these scripts again includes a script /etc/rc.subrthat loads the variables related to it. And this makes it possible to make even more modular configuration files: it’s enough to include /etc/rc.conf.dcode like the following in the main file in the directory (immediately using unicorn as an example):
unicorn_enable="YES"
unicorn_profiles=""
for p in $(grep -rlE '^unicorn_[0-9a-zA-Z]+_enabled="[Yy][Ee][Ss]"$' /usr/local/etc/unicorn.d); do
	bn=$(basename $p)
	if [ -n "$unicorn_profiles" ]; then
		unicorn_profiles="$unicorn_profiles $bn"
	else
		unicorn_profiles="$bn"
	fi
	. $p
done


Unicorn

First of all, we sudo’ll put it - this is much more convenient than “sculpting” a command to run through su, asking for shell injection.
cd /usr/ports/security/sudo && make install clean

We will also put a utility called portmaster - this will facilitate the further process:
cd /usr/ports/ports-mgmt/portmaster && make install clean

We will install the rest of the software with its help:
LN='ln -f' portmaster devel/bison textproc/libxml2 textproc/libxslt textproc/diffutils devel/gmake security/openssl devel/automake devel/git devel/subversion shells/bash

For the lazy - setting the default environment:
portmaster lang/ruby19 sysutils/rubygem-bundler www/rubygem-unicorn

A description of further steps can be found in the original article, and we move on to the next part - installing all this stuff as a service.

In order to be able to manage multiple instances of the service, the following code must be included in the startup script:
is_unicorn_profile() {
    local profile
    for profile in $unicorn_profiles; do
        if [ "$profile" = "$1" ]; then
            return 0
        fi
    done
    return 1
}
if [ -n "${unicorn_profiles}" ]; then
        if [ -n "$2" ]; then
                profile="$2"
                if ! is_unicorn_profile $profile; then
                        echo "$0: no such profile defined in unicorn_profiles."
                    exit 1
                fi
                eval unicorn_socket=\${unicorn_${profile}_socket:-"/tmp/${name}-${profile}.sock"}
                eval unicorn_config=\${unicorn_${profile}_config}
                eval unicorn_dir=\${unicorn_${profile}_dir}
                eval unicorn_flags=\${unicorn_${profile}_flags:-"${unicorn_flags}"}
                eval unicorn_environment=\${unicorn_${profile}_environment:-"${unicorn_environment}"}
                eval unicorn_rails=\${unicorn_${profile}_rails:-"${unicorn_rails}"}
                eval unicorn_user=\${unicorn_${profile}_user:-"${unicorn_user}"}
                eval unicorn_procname=\${unicorn_${profile}_procname:-"${unicorn_procname}"}
                eval unicorn_bundler=\${unicorn_${profile}_bundler:-"${unicorn_bundler}"}
                eval unicorn_rvm=\${unicorn_${profile}_rvm:-"${unicorn_rvm}"}
                eval unicorn_ruby=\${unicorn_${profile}_ruby:-"${unicorn_ruby}"}
                if checkyesno unicorn_rvm; then
                        unicorn_procname="~${unicorn_user}/.rvm/rubies/ruby-${unicorn_ruby}/bin/ruby"
                fi
        elif [ -n "$1" ]; then
                for profile in ${unicorn_profiles}; do
                echo "Processing ${name} profile: ${profile}"
                $0 $1 ${profile}
            done
            exit 0
        fi
fi

Using RVM poses a problem: for different applications it is necessary to fine-tune the environment. You can, of course, after starting each instance, reinitialize the environment, but it's easier to use a helper script:
if checkyesno unicorn_rvm; then
        if [ -d "~${unicorn_user}/.rvm/${unicorn_ruby}" ]; then
                /usr/local/bin/sudo -u $unicorn_user /usr/bin/env SHELL=/usr/local/bin/bash CDPATH=.  /usr/local/bin/unicorn-wrapper ${unicorn_ruby} $(basename ${command}) ${command_args}
                rc=$?
        else
                echo "Ruby version ${unicorn_ruby} not found by RVM for user ${unicorn_user}"
                rc=1
        fi
else
        /usr/local/bin/sudo -u $unicorn_user ${command} ${command_args}
        rc=$?
fi

During testing, it turned out that if ZSH is used, it is necessary to add to the list of redefinable variables ZSH_VERSION="". A file /usr/local/bin/unicorn-wrapperis a simple wrapper that installs the desired version of Ruby and executes a given command with arguments. The full version of the startup script can be taken nearby . Using is very simple:
service unicorn  []

Config example:
unicorn_redmine_enabled="YES"
unicorn_redmine_dir="/var/www/sites/mycoolsite.tld/redmine"
unicorn_redmine_rails="NO"

In a good way, it would be necessary to issue a Port Request, but so far the hands have not reached.

PS Only one question remained open: how can I integrate this stuff with the same capistrano so that the settings do not fly off during the update?

Also popular now: