Configuring puma init.d scripts and pumactl on debian with rvm

I’ve been building a trial version of our production environment, hosted on AWS.  I’m using debian as it’s what I’m most familiar with, and I’ve chosen to use puma to serve the rails end of the site.

Puma comes with an init.d script, and a control process called pumactl.  These were non-trivial to get working, particularly when also using rvm, so I thought I’d document what I did in case someone else wants to do the same.  The script uses great terminology based around the puma theme, so we have a jungle of pumas which may or may not come out to play.  In essence you can have multiple puma instances on your server, each serving a different application.

To start with you install puma into your rails project:

gem install puma

This will install the puma gem and various other pieces.  You need a config file in config/puma.rb, this didn’t get created by default when I installed puma.  You can get a sample config file from github.

I’ll first talk about what you need to configure and why, then show my config file.

  1. In theory you can set a path at the top, but it seemed to me that only some of the config below actually used this path – particularly the sockets and pids seemed to get left out.  I’m choosing to ignore the path setting and use absolute paths everywhere
  2. My server runs a custom environment ‘sandbox’ – so I’m not production yet, but I’m also not dev or test.  So I set the environment to sandbox
  3. I want the init.d script to be able to start and stop my puma, and to do so it relies on knowing the pid of this instance within the jungle.  So we need to store the pid somewhere, on rails 4 the place seems to be tmp/pids/puma.pid
  4. I want to use pumactl to start and stop things, and to get at status on my jungle.  So I also set a state file, I put that in tmp/pids/puma.state
  5. I want a few threads to start on application startup – my application is light usage, and I don’t want it going all the way down to 0 threads, so I set a 3 thread minimum
  6. I’m serving via nginx, and nginx likes to communicate with puma over a socket – so I ask puma to bind to a socket in tmp/sockets/puma.sock
  7. Amazon handles ssl for me, so no ssl settings needed (and if Amazon didn’t, then nginx would probably do it)
  8. I want to use the control app, and despite indicating that it can just use the pid to get at the running puma, it seems to not work without a control app activated, I set this on another socket tmp/sockets/pumactl.sock

This all gives me a config file like:

#!/usr/bin/env puma
# The directory to operate out of.
#
# The default is the current directory.
#
# directory '/u/apps/lolcat'

# Use an object or block as the rack application. This allows the
# config file to be the application itself.
#
# app do |env|
# puts env
#
# body = 'Hello, World!'
#
# [200, { 'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s }, [body]]
# end

# Load “path” as a rackup file.
#
# The default is “config.ru”.
#
# rackup '/u/apps/lolcat/config.ru'

# Set the environment in which the rack's app will run. The value must be a string.
#
# The default is “development”.
#
environment 'sandbox'

# Daemonize the server into the background. Highly suggest that
# this be combined with “pidfile” and “stdout_redirect”.
#
# The default is “false”.
#
# daemonize
# daemonize false

# Store the pid of the server in the file at “path”.
#
# pidfile '/u/apps/lolcat/tmp/pids/puma.pid'
pidfile '/usr/share/rails/sandbox/tmp/pids/puma.pid'

# Use “path” as the file to store the server info state. This is
# used by “pumactl” to query and control the server.
#
# state_path '/u/apps/lolcat/tmp/pids/puma.state'
state_path '/usr/share/rails/sandbox/tmp/pids/puma.state'

# Redirect STDOUT and STDERR to files specified. The 3rd parameter
# (“append”) specifies whether the output is appended, the default is
# “false”.
#
# stdout_redirect '/u/apps/lolcat/log/stdout', '/u/apps/lolcat/log/stderr'
# stdout_redirect '/u/apps/lolcat/log/stdout', '/u/apps/lolcat/log/stderr', true

# Disable request logging.
#
# The default is “false”.
#
# quiet

# Configure “min” to be the minimum number of threads to use to answer
# requests and “max” the maximum.
#
# The default is “0, 16”.
#
# threads 0, 16
threads 3, 16

# Bind the server to “url”. “tcp://”, “unix://” and “ssl://” are the only
# accepted protocols.
#
# The default is “tcp://0.0.0.0:9292”.
#
# bind 'tcp://0.0.0.0:9292'
# bind 'unix:///var/run/puma.sock'
# bind 'unix:///var/run/puma.sock?umask=0777'
# bind 'ssl://127.0.0.1:9292?key=path_to_key&cert=path_to_cert'
bind 'unix:///usr/share/rails/sandbox/tmp/sockets/puma.sock'

# Instead of “bind 'ssl://127.0.0.1:9292?key=path_to_key&cert=path_to_cert'” you
# can also use the “ssl_bind” option.
#
# ssl_bind '127.0.0.1', '9292', { key: path_to_key, cert: path_to_cert }

# Code to run before doing a restart. This code should
# close log files, database connections, etc.
#
# This can be called multiple times to add code each time.
#
# on_restart do
# puts 'On restart...'
# end

# Command to use to restart puma. This should be just how to
# load puma itself (ie. 'ruby -Ilib bin/puma'), not the arguments
# to puma, as those are the same as the original process.
#
# restart_command '/u/app/lolcat/bin/restart_puma'

# === Cluster mode ===

# How many worker processes to run.
#
# The default is “0”.
#
# workers 2

# Code to run when a worker boots to setup the process before booting
# the app.
#
# This can be called multiple times to add hooks.
#
# on_worker_boot do
# puts 'On worker boot...'
# end

# Code to run when a worker boots to setup the process after booting
# the app.
#
# This can be called multiple times to add hooks.
#
# after_worker_boot do
# puts 'On worker boot...'
# end

# Allow workers to reload bundler context when master process is issued
# a USR1 signal. This allows proper reloading of gems while the master
# is preserved across a phased-restart. (incompatible with preload_app)
# (off by default)

# prune_bundler

# Preload the application before starting the workers; this conflicts with
# phased restart feature. (off by default)

# preload_app!

# Additional text to display in process listing
#
# tag 'app name'

# Change the default timeout of workers
#
# worker_timeout 60

# === Puma control rack application ===

# Start the puma control rack application on “url”. This application can
# be communicated with to control the main server. Additionally, you can
# provide an authentication token, so all requests to the control server
# will need to include that token as a query parameter. This allows for
# simple authentication.
#
# Check out https://github.com/puma/puma/blob/master/lib/puma/app/status.rb
# to see what the app has available.
#
# activate_control_app 'unix:///var/run/pumactl.sock'
# activate_control_app 'unix:///var/run/pumactl.sock', { auth_token: '12345' }
# activate_control_app 'unix:///var/run/pumactl.sock', { no_token: true }
activate_control_app 'unix:///usr/share/rails/sandbox/tmp/sockets/pumactl.sock'

We should now be able to start and stop our server from within our rails directory (in my case, /usr/share/rails/sandbox).  I’m assuming you already had puma running from the command line, so try out the pumactl commands:

  pumactl start
  pumactl restart
  pumactl stats

Next, we’d like our puma to be started when we reboot our server, and to be able to use commands like

/etc/init.d/puma restart

There are three pieces that I needed to do this:

  1. An init.d script.  The base script can be found on github, I did some modifications which I’ve provided below, and it goes into /etc/init.d/puma
  2. A run-puma script from github, which goes into /usr/local/bin/run-puma.  This is essentially a wrapper that sets the various ruby and rvm variables before running anything, I modified it a bit to work with rvm, copy provided below
  3. A run-pumactl script, which I based off the run-puma script.  Pumactl also needs some variables set, it isn’t installed globally, so the base init.d script wasn’t working for me. This is also below.

Starting with the init.d script, I made a few minor modifications.  The big ones were to parameterise the location of the pids and the statefile, and to call run-pumactl.  I also changed the log_daemon_msg commands to log_action_message so that they had newlines after them and generally looked prettier, including logging a little more information so I could see what was going on.

#! /bin/sh
### BEGIN INIT INFO
# Provides:          puma 
# Required-Start:    $remote_fs $syslog $networking
# Required-Stop:     $remote_fs $syslog $networking
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Example initscript
# Description:       This file should be used to construct scripts to be
#                    placed in /etc/init.d.
### END INIT INFO

# Author: Darío Javier Cravero <dario@exordo.com>
#
# Do NOT "set -e"

# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/usr/local/bin:/usr/local/sbin/:/sbin:/usr/sbin:/bin:/usr/bin
DESC="Puma rack web server"
NAME=puma
DAEMON=$NAME
SCRIPTNAME=/etc/init.d/$NAME
CONFIG=/etc/puma.conf
JUNGLE=`cat $CONFIG`
RUNPUMA=/usr/local/bin/run-puma
RUNPUMACTL=/usr/local/bin/run-pumactl
PIDPATH=tmp/pids/puma.pid
STATEPATH=tmp/pids/puma.state

# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh

# Define LSB log_* functions.
# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
. /lib/lsb/init-functions

#
# Function that starts the jungle 
#
do_start() {
  log_action_msg "=> Running the jungle..."
  for i in $JUNGLE; do
    dir=`echo $i | cut -d , -f 1`
    user=`echo $i | cut -d , -f 2`
    config_file=`echo $i | cut -d , -f 3`
    if [ "$config_file" = "" ]; then
      config_file="$dir/config/puma.rb"
    fi
    log_file=`echo $i | cut -d , -f 4`
    if [ "$log_file" = "" ]; then
      log_file="$dir/log/puma.log"
    fi
    do_start_one $dir $user $config_file $log_file
  done
  log_action_msg "=> Jungle is awake"
}

do_start_one() {
  PIDFILE=$1/$PIDPATH
  if [ -e $PIDFILE ]; then
    PID=`cat $PIDFILE`
    # If the puma isn't running, run it, otherwise restart it.
    if [ "`ps -A -o pid= | grep -c $PID`" -eq 0 ]; then
      do_start_one_do $1 $2 $3 $4
    else
      do_restart_one $1
    fi
  else
    do_start_one_do $1 $2 $3 $4
  fi
}

do_start_one_do() {
  log_action_msg "--> Woke up puma $1"
  log_action_msg "  user $2"
  log_action_msg "  config $3"
  log_action_msg "  log to $4"
  start-stop-daemon --verbose --start --chdir $1 --chuid $2 --background --exec $RUNPUMA -- $1 $3 $4
}

#
# Function that stops the jungle
#
do_stop() {
  log_action_msg "=> Putting all the beasts to bed..."
  for i in $JUNGLE; do
    dir=`echo $i | cut -d , -f 1`
    do_stop_one $dir
  done
  log_action_msg "=> Jungle is quiet"
}
#
# Function that stops the daemon/service
#
do_stop_one() {
  PIDFILE=$1/$PIDPATH
  STATEFILE=$1/$STATEPATH
  log_action_msg "--> Stopping $1"
  if [ -e $PIDFILE ]; then
    PID=`cat $PIDFILE`
    if [ "`ps -A -o pid= | grep -c $PID`" -eq 0 ]; then
      log_warning_msg "---> Puma $1 isn't running."
    else
      log_action_msg "---> About to kill PID `cat $PIDFILE`"
      $RUNPUMACTL $1 $STATEFILE stop
      # Many daemons don't delete their pidfiles when they exit.
      rm -f $PIDFILE $STATEFILE
    fi
  else
    log_warning_msg "---> No puma here...$PIDFILE"
  fi
  return 0
}

#
# Function that restarts the jungle 
#
do_restart() {
  for i in $JUNGLE; do
    dir=`echo $i | cut -d , -f 1`
    do_restart_one $dir
  done
  log_action_msg "=> Jungle has napped"
}

#
# Function that sends a SIGUSR2 to the daemon/service
#
do_restart_one() {
  PIDFILE=$1/$PIDPATH
  STATEFILE=$1/$STATEPATH
  i=`grep $1 $CONFIG`
  dir=`echo $i | cut -d , -f 1`
  
  if [ -e $PIDFILE ]; then
    log_action_msg "--> About to restart puma $1"
    $RUNPUMACTL $1 $STATEFILE restart
    # kill -s USR2 `cat $PIDFILE`
    # TODO Check if process exist
  else
    log_warning_msg "--> Your puma was never playing... Let's get it out there first...$PIDFILE" 
    user=`echo $i | cut -d , -f 2`
    config_file=`echo $i | cut -d , -f 3`
    if [ "$config_file" = "" ]; then
      config_file="$dir/config/puma.rb"
    fi
    log_file=`echo $i | cut -d , -f 4`
    if [ "$log_file" = "" ]; then
      log_file="$dir/log/puma.log"
    fi
    do_start_one $dir $user $config_file $log_file
  fi
	return 0
}

#
# Function that statuses the jungle 
#
do_status() {
  for i in $JUNGLE; do
    dir=`echo $i | cut -d , -f 1`
    do_status_one $dir
  done
  log_action_msg "=> Jungle has roared"  
}

#
# Function that sends a SIGUSR2 to the daemon/service
#
do_status_one() {
  PIDFILE=$1/$PIDPATH
  STATEFILE=$1/$STATEPATH

  i=`grep $1 $CONFIG`
  dir=`echo $i | cut -d , -f 1`
  
  if [ -e $PIDFILE ]; then
    log_action_msg "--> About to status puma $1"
    $RUNPUMACTL $1 $STATEFILE stats
    # kill -s USR2 `cat $PIDFILE`
    # TODO Check if process exist
  else
    log_warning_msg "--> $1 isn't there :(..." 
  fi
	return 0
}

do_add() {
  str=""
  # App's directory
  if [ -d "$1" ]; then
    if [ "`grep -c "^$1" $CONFIG`" -eq 0 ]; then
      str=$1
    else
      echo "The app is already being managed. Remove it if you want to update its config."
      exit 1 
    fi
  else
    echo "The directory $1 doesn't exist."
    exit 1
  fi
  # User to run it as
  if [ "`grep -c "^$2:" /etc/passwd`" -eq 0 ]; then
    echo "The user $2 doesn't exist."
    exit 1
  else
    str="$str,$2"
  fi
  # Config file
  if [ "$3" != "" ]; then
    if [ -e $3 ]; then
      str="$str,$3"
    else
      echo "The config file $3 doesn't exist."
      exit 1
    fi
  fi
  # Log file
  if [ "$4" != "" ]; then
    str="$str,$4"
  fi

  # Add it to the jungle 
  echo $str >> $CONFIG
  log_action_msg "Added a Puma to the jungle: $str. You still have to start it though."
}

do_remove() {
  if [ "`grep -c "^$1" $CONFIG`" -eq 0 ]; then
    echo "There's no app $1 to remove."
  else
    # Stop it first.
    do_stop_one $1
    # Remove it from the config.
    sed -i "\\:^$1:d" $CONFIG
    log_action_msg "Removed a Puma from the jungle: $1."
  fi
}

case "$1" in
  start)
    [ "$VERBOSE" != no ] && log_action_msg "Starting $DESC" "$NAME" 
    if [ "$#" -eq 1 ]; then
      do_start
    else
      i=`grep $2 $CONFIG`
      dir=`echo $i | cut -d , -f 1`
      user=`echo $i | cut -d , -f 2`
      config_file=`echo $i | cut -d , -f 3`
      if [ "$config_file" = "" ]; then
        config_file="$dir/config/puma.rb"
      fi
      log_file=`echo $i | cut -d , -f 4`
      if [ "$log_file" = "" ]; then
        log_file="$dir/log/puma.log"
      fi
      do_start_one $dir $user $config_file $log_file
    fi
    case "$?" in
      0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
      2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
    esac
  ;;
  stop)
    [ "$VERBOSE" != no ] && log_action_msg "Stopping $DESC" "$NAME" 
    if [ "$#" -eq 1 ]; then
      do_stop
    else
      i=`grep $2 $CONFIG`
      dir=`echo $i | cut -d , -f 1`
      do_stop_one $dir
    fi
    case "$?" in
      0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
      2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
    esac
  ;;
  status)
    # TODO Implement.
    log_action_msg "Status $DESC" "$NAME" 
    if [ "$#" -eq 1 ]; then
      do_status
    else
      i=`grep $2 $CONFIG`
      dir=`echo $i | cut -d , -f 1`
      do_status_one $dir
    fi
    case "$?" in
      0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
      2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
    esac
  ;;
  restart)
    log_action_msg "Restarting $DESC" "$NAME" 
    if [ "$#" -eq 1 ]; then
      do_restart
    else
      i=`grep $2 $CONFIG`
      dir=`echo $i | cut -d , -f 1`
      do_restart_one $dir
    fi
    case "$?" in
      0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
      2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
    esac
  ;;
  add)
    if [ "$#" -lt 3 ]; then
      echo "Please, specifiy the app's directory and the user that will run it at least."
      echo "  Usage: $SCRIPTNAME add /path/to/app user /path/to/app/config/puma.rb /path/to/app/config/log/puma.log"
      echo "    config and log are optionals."
      exit 1
    else
      do_add $2 $3 $4 $5
    fi
    case "$?" in
      0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
      2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
    esac
  ;;
  remove)
    if [ "$#" -lt 2 ]; then
      echo "Please, specifiy the app's directory to remove."
      exit 1
    else
      do_remove $2
    fi
    case "$?" in
      0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
      2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
    esac
  ;;
  *)
    echo "Usage:" >&2
    echo "  Run the jungle: $SCRIPTNAME {start|stop|status|restart}" >&2
    echo "  Add a Puma: $SCRIPTNAME add /path/to/app user /path/to/app/config/puma.rb /path/to/app/config/log/puma.log"
    echo "    config and log are optionals."
    echo "  Remove a Puma: $SCRIPTNAME remove /path/to/app"
    echo "  On a Puma: $SCRIPTNAME {start|stop|status|restart} PUMA-NAME" >&2
    exit 3
  ;;
esac
:

This script needs to be copied to /etc/init.d/puma, and have permissions of 755.

Next, the run-puma script.  The only substantive change is to run the rvm profile file first, nothing works without it if you’re using rvm.

#!/bin/bash
app=$1; config=$2; log=$3;

source /etc/profile.d/rvm.sh
cd $app
exec bundle exec puma -C $config 2>&1 >> $log

This script goes into /usr/local/bin, and again permissions 755.

Last, the run-pumactl script.  Again, the main thing here is to wrapper the rvm profile before calling run-pumactl.

#!/bin/bash
APP=$1; STATEFILE=$2; CMD=$3; 

source /etc/profile.d/rvm.sh
cd $APP
exec bundle exec pumactl --state $STATEFILE $CMD 

Again into /usr/local/bin and permissions 755.

From there, I was able to execute /etc/init.d/puma {command}, and my puma started and stopped with server start and stop. This is important on Amazon, as my aim is to be able to auto-launch new instances when I wish without needing to actually log on to the machine for anything – I just add it to the load balancer and have more capacity available.

Advertisements

7 thoughts on “Configuring puma init.d scripts and pumactl on debian with rvm

  1. Thanks so much for this. The extant documentation for installing rails apps via init.d is lacking, at best. This is very helpful.

  2. In the puma file placed in init.d, it references a puma.conf placed in /etc. Where did you get this, and how should it look like?

  3. I should also mention that I use rbenv and not rvm, which causes some problems with these scripts.

  4. It’s been a while for this stuff, and I’m not near my servers at the moment. But is the first file in this post not the config file?

  5. That is probably it, it was just never stated that it was to be named puma.conf. I see now. Still, problems with the rvm and rbenv though. But thank you for response.

  6. Never mind, that is the puma.rb inside the application folder. The puma.conf is placed in the /etc directory.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s