How to Deploy Ruby on Rails 7/8 with Puma and Nginx on Ubuntu 24.04

Deploying a modern Ruby on Rails 7 or Rails 8 web application in production requires a resilient multi-tier infrastructure. While managed platforms offer quick deployments, hosting Rails directly on an enterprise Linux cloud VPS delivers unmatched cost efficiency, compute predictability, and granular architectural control. In this definitive guide, we walk through setting up a complete production stack: rbenv, PostgreSQL, Puma clustered application server, systemd daemon management, and Nginx HTTP/2 reverse proxying with automated SSL encryption.

1. Rails Production Architecture Overview

Modern Rails applications handle concurrent traffic through a battle-tested architecture that isolates static asset delivery from dynamic Ruby process execution:

  • Client Layer: Public HTTPS requests enter through port 443 with TLS 1.3 termination.
  • Nginx Reverse Proxy: Directly serves static assets (precompiled CSS, JavaScript, WebP images from public/) with aggressive caching headers, and forwards dynamic requests through a Unix domain socket.
  • Puma Clustered Server: Manages worker processes and threads using Ruby’s concurrent runtime, executing ActiveRecord queries and template rendering.
  • PostgreSQL: Persistent relational database running with connection pooling and optimized buffers.
  • Redis & Solid Queue / Sidekiq: Asynchronous background job processing for transactional emails, webhooks, and heavy calculations.

2. Server Prerequisites and Base Tooling

Update your Ubuntu 24.04 LTS operating system and install the essential build toolchains required to compile Ruby native extensions:

sudo apt update && sudo apt upgrade -y
sudo apt install -y git curl libssl-dev libreadline-dev zlib1g-dev autoconf     bison build-essential libyaml-dev libreadline-dev libncurses5-dev     libffi-dev libgdbm-dev nginx postgresql postgresql-contrib libpq-dev redis-server

Configuring Dedicated Deployment User

Never run Ruby on Rails applications under the root account. Create an isolated system user named deploy:

sudo adduser --disabled-password --gecos "" deploy
sudo usermod -aG sudo deploy
sudo su - deploy

Installing Ruby via rbenv

Using rbenv ensures clean version management without interfering with system-level packages:

git clone https://github.com/rbenv/rbenv.git ~/.rbenv
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
source ~/.bashrc

git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
rbenv install 3.3.5
rbenv global 3.3.5
gem install bundler --no-document

3. Database Provisioning & Environment Configuration

Switch to the PostgreSQL console to provision a secure database user and production schema:

sudo -u postgres psql

CREATE ROLE deploy_user WITH LOGIN PASSWORD 'SuperSecretStrongDBPass2026!';
ALTER ROLE deploy_user CREATEDB;
CREATE DATABASE rails_production OWNER deploy_user;
\q

Clone your Rails application repository into /var/www/rails_app and configure production credentials or an encrypted .env file:

cd /var/www/rails_app
bundle config set --local deployment 'true'
bundle config set --local without 'development test'
bundle install

# Precompile assets and migrate database
RAILS_ENV=production bundle exec rails db:migrate
RAILS_ENV=production bundle exec rails assets:precompile

4. Configuring Clustered Puma Application Server

Create or update config/puma.rb to optimize thread pools and process clustering matching your VPS CPU cores:

# config/puma.rb
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

workers ENV.fetch("WEB_CONCURRENCY") { 2 }
bind "unix:///var/www/rails_app/tmp/sockets/puma.sock"

environment ENV.fetch("RAILS_ENV") { "production" }
pidfile ENV.fetch("PIDFILE") { "/var/www/rails_app/tmp/pids/puma.pid" }
state_path "/var/www/rails_app/tmp/pids/puma.state"

preload_app!

on_worker_boot do
  ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
end

5. Managing Puma with systemd

Create a dedicated systemd service unit to handle automatic recovery, zero-downtime reloads, and boot-time startup at /etc/systemd/system/puma.service:

[Unit]
Description=Puma HTTP Server for Rails Application
After=network.target postgresql.service

[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/rails_app
ExecStart=/home/deploy/.rbenv/shims/bundle exec puma -C config/puma.rb
ExecReload=/bin/kill -USR1 $MAINPID
Restart=always
RestartSec=5s
Environment=RAILS_ENV=production
Environment=RAILS_MASTER_KEY=your_production_master_key_here

[Install]
WantedBy=multi-user.target

Reload systemd, enable the service, and verify runtime health:

sudo systemctl daemon-reload
sudo systemctl enable --now puma
sudo systemctl status puma

6. Configuring Nginx Reverse Proxy with Brotli & HTTP/2

Configure Nginx to serve static files directly and forward dynamic requests to Puma’s Unix domain socket at /etc/nginx/sites-available/rails_app.conf:

upstream puma_rails {
    server unix:///var/www/rails_app/tmp/sockets/puma.sock fail_timeout=0;
}

server {
    listen 80;
    server_name rails.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name rails.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/rails.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/rails.yourdomain.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    root /var/www/rails_app/public;
    client_max_body_size 50M;

    location / {
        try_files $uri @rails;
    }

    location @rails {
        proxy_pass http://puma_rails;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Host $http_host;
        proxy_redirect off;
    }

    location ~ ^/(assets|packs)/ {
        gzip_static on;
        expires max;
        add_header Cache-Control public;
    }
}

7. Production Diagnostics & Troubleshooting Matrix

Symptom Probable Root Cause Remediation Command
Nginx 502 Bad Gateway Puma socket permissions or process not running sudo systemctl restart puma && ls -l /var/www/rails_app/tmp/sockets/
Missing Master Key Error config/master.key missing or not passed in systemd Pass RAILS_MASTER_KEY in puma.service
PG::ConnectionBad: connection refused PostgreSQL service inactive or credentials mismatch sudo systemctl restart postgresql && pg_isready

8. Kamal Deployment vs Systemd: Choosing Your Rails Deployment Strategy

With Rails 8 embracing Kamal as the official zero-downtime containerized deployment tool, engineers have two production pathways on Linux cloud VPS:

  • Systemd + rbenv (Bare-Metal Speed): As configured in this tutorial, running directly on Ubuntu via systemd provides minimal memory overhead, zero container abstraction penalties, and instant access to system-level profiling tools (perf, htop). Ideal for single-server setups where every megabyte of RAM matters.
  • Kamal (Dockerized Multi-Server Fleet): Kamal deploys your application in Docker containers via SSH, orchestrating Traefik reverse proxying and automated rolling updates across multiple VPS instances. It simplifies horizontal scaling when your Rails app expands to separate web, worker, and database nodes.

9. Active Storage Image Optimization with libvips

Modern Rails applications handle extensive image processing for user uploads. Defaulting to ImageMagick introduces heavy memory spikes. Swap to libvips by adding gem 'image_processing', '~> 1.2' and setting in config/environments/production.rb:

config.active_storage.variant_processor = :vips

libvips processes high-resolution images up to 8x faster while consuming one-tenth the memory of legacy processing libraries.

Scale Rails Applications Seamlessly on CpanelFree VPS

Deploy high-concurrency Ruby on Rails apps with guaranteed CPU cores, blistering NVMe storage, and isolated memory architecture at affordable prices.

Launch High-Performance Rails VPS →

Leave a Comment