Post navigation

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.

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

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

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

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

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

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

Amazon handles ssl for me, so no ssl settings needed (and if Amazon didn’t, then nginx would probably do it)

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:

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

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

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.

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.