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