I was learning to deploy a demo rails app using Capistrano
, with Puma
as an app server and Nginx
web server accordingly. I've set up the necessary puma configurations in a file, _stage.rb
, and later set puma
as a SysVinit
service as /etc/init.d/puma_myarticles_staging
. The executable file, puma_init.sh.erb
was later written into the remote server as puma_init.sh
which looked like,
#!/usr/bin/env bash
PATH=/usr/local/bin:/usr/local/sbin/:/sbin:/usr/sbin:/bin:/usr/bin
DESC="Puma rack web server"
NAME=puma_<%=fetch(:full_app_name)%>
SCRIPT_NAME=/etc/init.d/${NAME}
APP_ROOT=<%=current_path%>
PIDFILE=<%= fetch(:puma_pid) %>
STATE_FILE=<%= fetch(:puma_state) %>
log_daemon_msg() { echo "$@"; }
log_end_msg() { [ $1 -eq 0 ] && RES=OK; logger ${RES:=FAIL}; }
run_pumactl(){
[ $# -lt 1 ] && echo "$# params were given, Expected 1" && exit 1
cd ${APP_ROOT} && <%= fetch(:rbenv_prefix) %> bundle exec pumactl -F <%=fetch(:puma_conf)%> $1
}
# Function that starts the puma
#
start_task() {
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_task
else
restart_task
fi
else
do_start_task
fi
}
do_start_task() {
log_daemon_msg "--> Woke up puma ${APP_ROOT}"
run_pumactl start
}
# Function that stops the daemon/service
#
stop_task() {
log_daemon_msg "--> Stopping puma in path: ${APP_ROOT} ..."
if [ -e ${PIDFILE} ]; then
PID=`cat ${PIDFILE}`
if [ "`ps -A -o pid= | grep -c ${PID}`" -eq 0 ]; then
log_daemon_msg "--> Puma isn't running in path: ${APP_ROOT}."
else
log_daemon_msg "--> About to kill puma with PID: `cat $PIDFILE` ..."
if [ "`ps -A -o pid= | grep -c ${PID}`" -eq 0 ]; then
log_daemon_msg "--> Puma isn't running in path: ${APP_ROOT}."
return 0
else
run_pumactl stop
log_daemon_msg "--> Waiting for status ..."
sleep 5
if [ "`ps -A -o pid= | grep -c ${PID}`" -eq 0 ]; then
log_daemon_msg "--> Puma with pid ${PID} stopped successfully."
rm -f ${PIDFILE} ${STATE_FILE}
else
log_daemon_msg "--> Unable to stop puma with pid ${PID}."
fi
fi
fi
else
log_daemon_msg "--> Puma isn't running in path: ${APP_ROOT}."
fi
return 0
}
# Function that sends a SIGUSR2 to the daemon/service
#
restart_task() {
if [ -e ${PIDFILE} ]; then
log_daemon_msg "--> About to restart puma in path: ${APP_ROOT} ..."
run_pumactl restart
else
log_daemon_msg "--> Your puma was never playing... Let's get it out there first ..."
start_task
fi
return 0
}
# Function that sends a SIGUSR2 to the daemon/service
#
status_task() {
if [ -e ${PIDFILE} ]; then
log_daemon_msg "--> About to status puma ${APP_ROOT} ..."
run_pumactl status
else
log_daemon_msg "---> Puma isn't running in path: ${APP_ROOT}."
fi
return 0
}
case "$1" in
start)
[ "$VERBOSE" != no ] && log_daemon_msg "Starting ${DESC}" "${NAME} ..."
start_task
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
stop)
[ "$VERBOSE" != no ] && log_daemon_msg "Stopping ${DESC}" "${NAME} ..."
stop_task
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
status)
log_daemon_msg "Status ${DESC}" "${NAME} ..."
status_task
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
restart)
log_daemon_msg "Restarting ${DESC}" "${NAME} ..."
restart_task
case "$?" in
0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
esac
;;
*)
echo "Usage:" >&2
echo " ${SCRIPT_NAME} {start|stop|status|restart}" >&2
exit 3
;;
esac
:
The puma.rb
was,
#!/usr/bin/env puma
directory '/app/myarticles_staging/current'
environment 'staging'
pidfile '/app/myarticles_staging/shared/tmp/pids/puma.pid'
state_path '/app/myarticles_staging/shared/tmp/states/puma.state'
stdout_redirect '/app/myarticles_staging/shared/log/puma_access.log', '/app/myarticles_staging/shared/log/puma_error.log', true
daemonize
threads 4, 8
bind 'unix:///app/myarticles_staging/shared/tmp/sockets/puma.myarticles_staging.sock'
activate_control_app 'unix:///app/myarticles_staging/shared/tmp/sockets/pumactl.myarticles_staging.sock'
workers '4'
preload_app!
on_worker_boot do
require "active_record"
ActiveRecord::Base.connection.disconnect! rescue ActiveRecord::ConnectionNotEstablished
ActiveRecord::Base.establish_connection(YAML.load_file('/app/myarticles_staging/shared/config/database.yml')['staging'])
end
# Allow puma to be restarted by the `rails restart` command.
plugin :tmp_restart
Where I'm fetching all the puma configurations from a file named _stage.rb
that looks like,
set :stage, :staging
set :branch, :staging
set :server_port, 80
set :full_app_name, "#{fetch(:application)}_#{fetch(:stage)}"
set :rails_env, :staging
set :deploy_to, "/app/#{fetch(:full_app_name)}"
set :puma_user, fetch(:deploy_user)
set :puma_state, "#{shared_path}/tmp/states/puma.state"
set :puma_pid, "#{shared_path}/tmp/pids/puma.pid"
set :puma_rackup, -> { File.join(current_path, 'config.ru')}
set :puma_bind, "unix://#{shared_path}/tmp/sockets/puma.#{fetch(:full_app_name)}.sock"
set :puma_default_control_app, "unix://#{shared_path}/tmp/sockets/pumactl.#{fetch(:full_app_name)}.sock"
set :puma_conf, "#{shared_path}/config/puma.rb"
set :puma_workers, 4
set :puma_threads, [4, 8]
set :puma_role, :app
set :puma_env, :staging
set :puma_preload_app, true
set :puma_enable_socket_service, true
set :puma_access_log, "#{shared_path}/log/puma_access.log"
set :puma_error_log, "#{shared_path}/log/puma_error.log"
set :nginx_access_log, "#{shared_path}/log/nginx_access.log"
set :nginx_error_log, "#{shared_path}/log/nginx_error.log"
When I started the puma service as /etc/init.d/puma_myarticles_staging start
, it outputs,
Starting Puma rack web server puma_myarticles_staging ...
--> Woke up puma /app/myarticles_staging/current
[3955] Puma starting in cluster mode...
[3955] * Version 4.3.12 (ruby 2.7.0-p0), codename: Mysterious Traveller
[3955] * Min threads: 4, max threads: 8
[3955] * Environment: staging
[3955] * Process workers: 1
[3955] * Preloading application
[3955] * Listening on unix:///app/myarticles_staging/shared/tmp/sockets/puma.myarticles_staging.sock
[3955] ! WARNING: Detected 1 Thread(s) started in app boot:
[3955] ! #<Thread:0x000055c692868bf8 /app/myarticles_staging/shared/bundle/ruby/2.7.0/gems/activerecord-6.1.7.4/lib/active_record/connection_adapters/abstract/connection_pool.rb:323 sleep> - /app/myarticles_staging/shared/bundle/ruby/2.7.0/gems/activerecord-6.1.7.4/lib/active_record/connection_adapters/abstract/connection_pool.rb:329:in `sleep'
[3955] * Daemonizing...
Leaving no new puma pid or state files behind. Eventually, the puma service workers didn't boot as I checked the puma.pid
and puma.state
there are no files being written. When I ran rbenv exec bundle exec rails s
to test manually on the current_path
it worked.
I checked for the puma process that was demonized using ps ax | grep puma
, but didn't find the actual puma workers,
1516 pts/0 S+ 0:00 grep --color=auto puma
Any suggestions on what I might be doing wrong? Thanks in advance.
Perhaps the issue is with the way puma is being started. I'm pretty certain there is no Puma bug here. You haven't posted any detail about your systemctl setup but best practice is to use a user service file that way puma will always be started on boot and no passwords are needed
A standard puma config that I use goes something like this
# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count
# Change to match your CPU core count
workers 0
# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
port ENV.fetch("PORT") { 3000 }
# Specifies the `environment` that Puma will run in.
#
rails_env = ENV['RAILS_ENV'] || "production"
environment rails_env
app_dir = File.expand_path("../..", __FILE__)
#shared_dir = "#{app_dir}/shared"
shared_dir = "/home/project/apps/comtech/shared" # Use your projects path
# Specifies the `pidfile` that Puma will use.
#pidfile ENV.fetch("PIDFILE") { "pids/server.pid" }
pidfile "#{shared_dir}/pids/puma.pid"
state_path "#{shared_dir}/pids/puma.state"
activate_control_app
# Set up socket location
bind "unix://#{shared_dir}/sockets/comtech_puma.sock"
on_worker_boot do
require "active_record"
ActiveRecord::Base.connection.disconnect! rescue ActiveRecord::ConnectionNotEstablished
ActiveRecord::Base.establish_connection(YAML.load_file("#{app_dir}/config/database.yml")[rails_env])
end
# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked web server processes. If using threads and workers together
# the concurrency of the application would be max `threads` * `workers`.
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
# process behavior so workers use less memory.
#
# preload_app!
# Allow puma to be restarted by `rails restart` command.
plugin :tmp_restart
Obviously you will need to set the ENVIRONMENT
variable to staging on the server
Adjust the pids folder accordingly to use tmp.
To use a user service rather than a system service take a look at the following
cd ~/.config/systemd/user
creating the folders if they don’t exist
nano name_of_your_puma.service
and paste following contents adjusting paths accordingly.
[Unit]
Description=Puma Rack Server
# Puma supports systemd's `Type=notify` and watchdog service
# monitoring, if the [sd_notify](https://github.com/agis/ruby-sdnotify) gem is installed,
# as of Puma 5.1 or later.
# On earlier versions of Puma or JRuby, change this to `Type=simple` and remove
# the `WatchdogSec` line.
[Service]
Type=simple
# If your Puma process locks up, systemd's watchdog will restart it within seconds.
#WatchdogSec=10
RestartSec=10
WorkingDirectory=/home/comtech/apps/comtech/current
#PIDFile=/home/comtech/apps/comtech/shared/pids/puma.pid
#User=comtech
ExecStart=/home/comtech/.rvm/bin/rvm 3.1.3@comtech_cms_app do bundle exec puma -C /home/comtech/apps/comtech/current/config/puma_production.rb
ExecStop=/home/comtech/.rvm/bin/rvm 3.1.3@comtech_cms_app do bundle exec pumactl -S /home/comtech/apps/comtech/shared/pids/puma.state stop
ExecReload=/home/comtech/.rvm/bin/rvm 3.1.3@ruby-3.1.3@comtech_cms_app do bundle exec pumactl -S /home/comtech/apps/comtech/shared/pids/puma.state restart
Restart=always
[Install]
WantedBy=multi-user.target
Then run
$ systemctl --user enable name_of_your_puma.service
To checkup on the service
$ systemctl --user status
To stop the service
$ systemctl --user stop
To start the service
$ systemctl --user start
The service will automatically start on boot and will restart puma if puma fails. Also note I' using RVM, so you need to adjust the start command accordingly