Introduction to Puma
Puma is a concurrent Ruby web server designed for high performance. It’s ideal for deploying Ruby on Rails applications due to its speed and efficiency in handling multiple requests simultaneously.
Configuring Mina
To streamline deployment, we’ll use Mina, a fast and efficient deployer for Ruby and Rails applications. Start by adding the mina-puma
gem to your Gemfile:
gem 'mina-puma', require: false
Install the gem:
bundle install
Initialize Mina within your project:
mina init
Next, add the following configuration to config/deploy.rb
:
# frozen_string_literal: true
require "mina/rails"
require "mina/git"
require "mina/rbenv"
set :application_name, "myapp"
set :domain, "yourdomain.com"
set :deploy_to, "/var/www/yourapp"
set :repository, "git@github.com:yourgithub/yourapp.git"
set :branch, "master"
set :user, "root"
set :shared_dirs, fetch(:shared_dirs, []).push("tmp/sockets", "tmp/pids", "log")
set :shared_files, fetch(:shared_files, []).push("config/credentials/production.key")
# Environment setup for remote commands
task :remote_environment do
invoke :'rbenv:load' # Load rbenv environment
# Load NVM environment
command %{
echo "-----> Loading NVM"
export PATH="$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH"
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && . "$NVM_DIR/bash_completion"
nvm use default
}
end
task :setup do
command %{rbenv install 3.2.2 --skip-existing}
command %{gem install bundler}
end
desc "Deploys the current version to the server."
task :deploy do
deploy do
invoke :'git:clone'
invoke :'deploy:link_shared_paths'
invoke :'bundle:install'
invoke :'rails:db_migrate'
invoke :'rails:assets_precompile'
invoke :'deploy:cleanup'
on :launch do
in_path(fetch(:current_path)) do
command %(mkdir -p tmp/)
command %(touch tmp/restart.txt)
end
end
end
end
namespace :puma do
desc "Start Puma"
task :start do
in_path(fetch(:current_path)) do
command "systemctl start puma.service"
end
end
desc "Restart Puma"
task :restart do
in_path(fetch(:current_path)) do
command "systemctl restart puma.service"
end
end
desc "Check Puma status"
task :status do
command "systemctl status puma.service"
command "journalctl -xeu puma.service"
end
desc "Tail Puma logs"
task :log do
command "journalctl -u puma.service -f"
end
desc "Setup Puma systemd service"
task :setup do
puts "Setting up systemd service for Puma.."
path = fetch(:deploy_to)
content = File.read("./lib/puma.service").gsub("<YOUR_APP_PATH>", path).gsub("<FULLPATH>", path)
File.write("./tmp/puma.service", content)
run(:local) do
command "scp ./tmp/puma.service #{fetch(:user)}@#{fetch(:domain)}:/etc/systemd/system/puma.service"
end
command "touch #{fetch(:deploy_to)}/shared/tmp/pids/server.pid"
command "systemctl daemon-reload"
command "systemctl enable puma.service"
end
end
desc "Copy production key to server"
task :copy_secrets do
run(:local) do
command "scp ./config/credentials/production.key #{fetch(:user)}@#{fetch(:domain)}:#{fetch(:deploy_to)}/shared/config/credentials/production.key"
end
end
Nginx Configuration
Nginx serves as a powerful reverse proxy and HTTP server for your Rails application. Here’s a sample configuration for Nginx:
upstream myapp {
server localhost:3000; # Define upstream server for myapp
}
server {
server_name be.example.com; # Server name for this nginx server block
keepalive_timeout 5; # Set keepalive timeout to 5 seconds
root /var/www/myapp/current/public; # Root directory for static files
access_log /var/www/myapp/shared/log/nginx.access.log; # Access log path
error_log /var/www/myapp/shared/log/nginx.error.log info; # Error log path and verbosity
if (-f $document_root/maintenance.html) {
rewrite ^(.*)$ /maintenance.html last; # Rewrite to maintenance page if it exists
break; # Stop further processing
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Set X-Forwarded-For header for proxy
proxy_set_header Host $host; # Set Host header for proxy
if (-f $request_filename) {
break; # Serve directly if file exists
}
if (-f $request_filename/index.html) {
rewrite (.*) $1/index.html break; # Rewrite to index.html if it exists
}
if (-f $request_filename.html) {
rewrite (.*) $1.html break; # Rewrite to .html if it exists
}
proxy_pass http://myapp; # Proxy pass to upstream myapp if no static file found
break; # Stop further processing
}
location ~* \.(ico|css|gif|jpe?g|png|js)(\?[0-9]+)?$ {
expires max; # Set max expiration for static files
break; # Stop further processing
}
# Error page configuration
location = /500.html {
root /var/www/myapp/current/public; # Serve 500.html from this directory
}
}
Systemd Configuration
Systemd manages services and processes on Linux systems. Here’s how to configure Puma as a systemd service:
Create a puma.service
file in your application’s root:
[Unit]
Description=Puma HTTP Server
After=network.target
# Uncomment for socket activation (see below)
# Requires=puma.socket
[Service]
# Puma supports systemd's `Type=notify` and watchdog service
# monitoring, as of Puma 5.1 or later.
# On earlier versions of Puma or JRuby, change this to `Type=simple` and remove
# the `WatchdogSec` line.
Type=notify
# If your Puma process locks up, systemd's watchdog will restart it within seconds.
WatchdogSec=10
# Preferably configure a non-privileged user
# User=
# The path to your application code root directory.
# Also replace the "<YOUR_APP_PATH>" placeholders below with this path.
# Example /home/username/myapp
WorkingDirectory=<YOUR_APP_PATH>
# Helpful for debugging socket activation, etc.
# Environment=PUMA_DEBUG=1
# SystemD will not run puma even if it is in your path. You must specify
# an absolute URL to puma. For example /usr/local/bin/puma
# Alternatively, create a binstub with `bundle binstubs puma --path ./sbin` in the WorkingDirectory
# ExecStart=<FULLPATH>/sbin/puma -C <YOUR_APP_PATH>/puma.rb
# Variant: Rails start.
Environment="PATH=/root/.rbenv/shims:/root/.rbenv/bin:/usr/local/bin:/usr/bin:/bin:/root/.nvm/versions/node/v22.3.0/bin/:$PATH"
#Environment="NVM_DIR="$HOME/.nvm";nvm use default"
ExecStart=<FULLPATH>/current/sbin/puma -e production --pidfile <YOUR_APP_PATH>/shared/tmp/pids/server.pid -C <YOUR_APP_PATH>/current/config/puma.rb <YOUR_APP_PATH>/current/config.ru
# Variant: Use `bundle exec puma` instead of binstub
# Variant: Specify directives inline.
# ExecStart=<FULLPATH>/puma -b tcp://0.0.0.0:9292 -b ssl://0.0.0.0:9293?key=key.pem&cert=cert.pem
Restart=always
[Install]
WantedBy=multi-user.target
Common Issues and Solutions
ExecJS::RuntimeUnavailable / /usr/bin/env: ‘ruby’: No such file or directory
Unable to load application: ExecJS::RuntimeUnavailable: Could not find a JavaScript runtime. See https://github.com/rails/execjs for a list of available runtimes.
I am using Bootstrap and its using autoprefixer-rails gem which uses execjs to process JS code.
Issue Explained
Since the systemd
initiates a separate non-interactive shell instance, which is clean and may not know about the existence of apps you install like nodejs, ruby, yarn, etc. so need to manually specify the path so adding this environment variable in puma.service
Environment="PATH=/root/.rbenv/shims:/root/.rbenv/bin:/usr/local/bin:/usr/bin:/bin:/root/.nvm/versions/node/v22.3.0/bin/:$PATH"
ExecStart=/var/www/myapp/current/sbin/puma -e production --pidfile /var/www/myapp/shared/tmp/pids/server.pid -C /var/www/myapp/current/config/puma.rb /var/www/myapp/current/config.ru