Catalyst::Wiki
/deployment/fcgi-init

Managing FastCGI Processes with an init script

While there are arguments for managing your FastCGI processes with a services manager like daemontools or perhaps upstart, there's a certain familiarity and simplicity to init scripts.

Wait, did I say simplicity? Maybe not... simple init scripts have a tendency to blow up in lots of interesting ways. Below is a not-so-simple script that I've developed for my own deployments that covers a lot of the bases and seems to work well, together with notes on usage.

Requirements

This script depends on... well not Debian exactly, but start-stop-daemon, which is rarely seen outside of Debian-like systems. It also depends on an LSB-compliant system. If you come up with a port to a system that does things differently, go ahead and post it below in its own section. Most importantly, it depends on FCGI::ProcManager 0.18, as previous versions don't handle being daemonized properly.

Good Stuff

This script supports:

  • Running the app setuid/setgid as its own user as a system startup script
  • Or running the app as the invoking user for personal project type deals.
  • local::lib
  • Best FHS practices for the pidfile location
  • Daemonizing the app instead of letting FCGI::ProcManager because FCGI::ProcManager does it wrong
  • Running the app through perl -c on restart to avoid embarrassment.

Configuration

  • Set APPNAME to the Perl class name of your application.
  • Set APPDIR to your app's root (or wherever you want it to start off chdir'd to)
  • Set PROCS to the number of FastCGI child processes to run.
  • Set SOCKET to the same thing it's set to in your webserver config. TCP and Unix sockets will work.
  • Set USER and GROUP to have the application run setuid/setgid. Leave USER unset (USER=) if you're not running as root, to run the app as the current user.
  • Only set PIDSUFFIX if you're running more than one app with the same APPNAME on a machine; you can use it to make sure that the instances get separate pidfiles.
  • Set LOCALLIB if you want to use a local::lib directory (this is better than setting it in the _fastcgi.pl script because the compile check on restart will work). Leave it blank if you're not using local::lib.

Note: If you have a slow server, the script may report that it has failed to start the daemon, even though it does eventually start successfully. This is because the part of the script that checks if the daemon is running gives up too soon. To fix this, just increase the delay in the _start() function from sleep 1 to sleep 2 (or greater).

The Repository

The latest version of this script can be found at https://github.com/arodland/cat-fcgi-init.

The Script

#!/bin/sh
# Start a Catalyst app under FastCGI
# Copyright (c) 2009-2010, Andrew Rodland
# See LICENSE for redistribution conditions.
### BEGIN INIT INFO
# Provides: webapp
# Required-Start: $local_fs $network $named
# Required-Stop: $local_fs $network $named
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: A Catalyst Application
### END INIT INFO 

. /lib/lsb/init-functions

APPNAME=MyApp
APPDIR=/home/myapp/MyApp
UNIXNAME=$(echo $APPNAME | perl -pe 's/::/_/;$_=lc')
PROCS=5
SOCKET=127.0.0.1:3001
# Leave these unset and we won't try to setuid/setgid.
USER=myapp
GROUP=myapp
# Set this if you have more than one instance of the app and you don't want
# them to step on each other's pidfile.
PIDSUFFIX=

# local::lib path, if you want to use it.
LOCALLIB=

if [ -f "/etc/default/"$UNIXNAME ]; then
    . "/etc/default/"$UNIXNAME
fi

if [ $(id -u) -eq 0 ] ; then
    PIDDIR=/var/run/$UNIXNAME
    mkdir $PIDDIR >/dev/null 2>&1
    chown $USER:$GROUP $PIDDIR
    chmod 775 $PIDDIR
else
    PIDDIR=/tmp
fi

PIDFILE=$PIDDIR/$UNIXNAME${PIDSUFFIX:+"-$PIDSUFFIX"}.pid

if [ -n "$LOCALLIB" ] ; then
    eval `perl -I"$LOCALLIB/lib/perl5" -Mlocal::lib="$LOCALLIB"`
fi

check_running() {
    [ -s $PIDFILE ] && kill -0 $(cat $PIDFILE) >/dev/null 2>&1
}

check_compile() {
    if [ -n "$USER" ] ; then
        if su $USER -c "cd $APPDIR ; perl -Ilib -M$APPNAME -ce1" ; then
            return 0
        fi
        return 1
    else
        if ( cd $APPDIR ; perl -Ilib -M$APPNAME -ce1 ) ; then
            return 0
        fi
        return 1
    fi
}

_start() {
    start-stop-daemon --start --quiet --pidfile $PIDFILE --chdir $APPDIR \
    ${USER:+"--chuid"} $USER ${GROUP:+"--group"} $GROUP --background \
    --startas $APPDIR/script/${UNIXNAME}_fastcgi.pl -- \ 
    -n $PROCS -l $SOCKET -p $PIDFILE

    for i in 1 2 3 4 5 6 7 8 9 10; do
        sleep 1
        if check_running ; then
            return 0
        fi
    done
    return 1
}

start() {
    log_daemon_msg "Starting $APPNAME" $UNIXNAME
    if check_running; then
        log_progress_msg "already running"
        log_end_msg 0
        exit 0
    fi

    rm -f $PIDFILE 2>/dev/null

    _start
    log_end_msg $?
    return $?
}

_stop() {
    start-stop-daemon --stop --user $USER --quiet --oknodo --pidfile $PIDFILE \
    --retry TERM/5/TERM/30/KILL/30 \
    || log_failure_message "It won't die!"
}

stop() {
    log_daemon_msg "Stopping $APPNAME" $UNIXNAME

    _stop
    log_end_msg $?
    return $?
}

restart() {
    log_daemon_msg "Restarting $APPNAME" $UNIXNAME

    check_compile && _stop && _start
    log_end_msg $?
    return $?
}

# See how we were called.
case "$1" in
    start)
        start
        ;;
    stop)
        stop
        ;;
    restart|force-reload)
        restart
        ;;
    check|check-compile)
        check_compile
        ;;
    *)
        echo $"Usage: $0 {start|stop|restart|check}"
        exit 1
esac
exit $?