Ruby on Rails Deployment with Puma and Nginx

Ruby on Rails is a full-stack web framework emphasizing developer productivity with convention over configuration, enabling rapid development of database-backed web applications. Deploying Rails applications requires Ruby runtime environment, bundled gem management, Puma application server, Nginx reverse proxy, PostgreSQL database, asset pipeline compilation, and background job processing. This guide covers production-ready Rails deployment including rbenv Ruby version manager, bundler dependency management, Puma configuration, Nginx proxy setup, PostgreSQL integration, static asset handling, systemd service management, and optimization for Linux servers.

Table of Contents

Rails Architecture Overview

Rails applications follow MVC pattern with models handling data, views rendering UI, and controllers coordinating logic. Understanding the architecture helps deployment configuration.

Request flow in Rails:

  1. Nginx receives HTTP request
  2. Nginx forwards to Puma via Unix socket
  3. Puma worker thread processes request
  4. Rails router resolves to controller action
  5. Controller interacts with models/database
  6. View template rendered
  7. Response sent back to Nginx
  8. Nginx returns to client

Key deployment considerations:

  • Rails runs with specific Ruby version (via rbenv)
  • Bundler manages gem dependencies
  • Database migrations set up schema
  • Asset pipeline compiles/minifies CSS/JavaScript
  • ActiveJob processes background jobs
  • Secrets management for sensitive data
  • Logging for monitoring and debugging

Ruby Environment Setup

Prepare system and install Ruby version manager.

Update system:

sudo apt update
sudo apt upgrade -y
sudo apt install curl wget git zip unzip vim htop build-essential -y

Install system dependencies:

# Required for compiling Ruby
sudo apt install libssl-dev libreadline-dev zlib1g-dev libsqlite3-dev \
    libpq-dev libffi-dev libyaml-dev -y

Install rbenv (Ruby version manager):

# Clone rbenv repository
git clone https://github.com/rbenv/rbenv.git ~/.rbenv

# Add to PATH
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc

# Reload shell
source ~/.bashrc

# Verify installation
rbenv --version

Install ruby-build plugin:

git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build

# Verify
rbenv install --list | head -20

Install Ruby 3.2:

# Install specific Ruby version (takes time to compile)
rbenv install 3.2.0

# Set as global version
rbenv global 3.2.0

# Verify
ruby --version

Install Bundler:

# Bundler is included with modern Ruby
# Verify
gem list bundler

# Update Bundler if needed
gem install bundler

Create application user:

sudo useradd -m -s /bin/bash rails
sudo usermod -aG www-data rails

# Create application directory
sudo mkdir -p /home/rails/app
sudo chown -R rails:www-data /home/rails/app

Install PostgreSQL:

sudo apt install postgresql postgresql-contrib libpq-dev -y
sudo systemctl start postgresql
sudo systemctl enable postgresql

Install Nginx:

sudo apt install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx

Rails Application Installation

Install and configure Rails application.

Clone or create Rails project:

cd /home/rails/app

# Clone from repository
sudo -u rails git clone https://github.com/yourname/rails-app.git .

# Or create new project
rails new . --database=postgresql

Configure Ruby version:

# Create .ruby-version file
echo "3.2.0" | sudo -u rails tee /home/rails/app/.ruby-version

# Verify
cd /home/rails/app
ruby --version

Create .env file:

sudo -u rails cat > /home/rails/app/.env << 'EOF'
RAILS_ENV=production
SECRET_KEY_BASE=your-secret-key-here
DATABASE_URL=postgresql://rails_user:password@localhost/rails_db
REDIS_URL=redis://127.0.0.1:6379/0
EOF

Generate secret key:

cd /home/rails/app
bundle exec rails secret

Update Gemfile if needed:

sudo -u rails nano /home/rails/app/Gemfile

Common production gems:

gem 'rails', '~> 7.0.0'
gem 'pg', '~> 1.1'
gem 'puma', '~> 6.0'
gem 'rack-cors'
gem 'redis', '~> 5.0'
gem 'sidekiq'

group :production do
  gem 'aws-sdk-s3'
  gem 'lograge'
end

Bundler and Gem Management

Install and manage gem dependencies.

Create Gemfile.lock:

cd /home/rails/app

# Install gems without development/test
sudo -u rails bundle install --jobs=$(nproc) --without development test

# Verify
ls -la Gemfile.lock

Configure gem home:

# Set for rails user
sudo -u rails cat >> ~/.bashrc << 'EOF'
export BUNDLE_PATH="/home/rails/.bundle"
export BUNDLE_BIN="/home/rails/.bundle/bin"
EOF

# Reload for rails user
sudo -u rails bash -c 'source ~/.bashrc'

Set permissions:

# Ensure web server can read gems
sudo chmod -R 755 /home/rails/app
sudo chown -R rails:www-data /home/rails/app

PostgreSQL Database

Set up PostgreSQL database and user.

Connect to PostgreSQL:

sudo -u postgres psql

Create database and user:

CREATE DATABASE rails_db;
CREATE USER rails_user WITH PASSWORD 'SecurePassword123!';
ALTER ROLE rails_user SET client_encoding TO 'utf8';
ALTER ROLE rails_user SET default_transaction_isolation TO 'read committed';
ALTER ROLE rails_user SET timezone TO 'UTC';
GRANT ALL PRIVILEGES ON DATABASE rails_db TO rails_user;
\q

Run migrations:

cd /home/rails/app
sudo -u rails bundle exec rails db:create
sudo -u rails bundle exec rails db:migrate

# Verify
sudo -u rails bundle exec rails db:prepare

Seed database (if seeders exist):

sudo -u rails bundle exec rails db:seed

Puma Application Server

Configure Puma to run Rails application.

Create Puma configuration:

sudo -u rails cat > /home/rails/app/config/puma.rb << 'EOF'
# Puma can serve each request in a thread from an internal thread pool

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

worker_count = ENV.fetch("WEB_CONCURRENCY") { 4 }

workers worker_count

worker_timeout 3600

pidfile ENV.fetch("PIDFILE") { "tmp/pids/puma.pid" }

bind ENV.fetch("BIND") { "unix:///tmp/puma.sock" }

environment ENV.fetch("RAILS_ENV") { "development" }

log_requests true
log_stdout true

# Allow pids and state files to be written to tmp directory
state_path "tmp/pids/puma.state"

plugin :tmp_restart

# Configure logging
stdout_redirect "log/puma.stdout.log", "log/puma.stderr.log", true
EOF

Or generate default Puma config:

cd /home/rails/app
sudo -u rails bundle exec puma -t 8:32 -w 4 --preload-app

Create systemd service:

sudo cat > /etc/systemd/system/puma.service << 'EOF'
[Unit]
Description=Puma HTTP Server
After=network.target

[Service]
Type=simple
User=rails
WorkingDirectory=/home/rails/app
Environment="RAILS_ENV=production"
Environment="BUNDLE_GEMFILE=/home/rails/app/Gemfile"

ExecStart=/home/rails/.rbenv/shims/bundle exec puma -c config/puma.rb
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

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

Verify Puma:

# Check socket created
ls -la /tmp/puma.sock

# Check process
ps aux | grep puma

# Check logs
tail -f /home/rails/app/log/puma.stdout.log

Nginx Reverse Proxy

Configure Nginx to proxy requests to Puma.

Create Nginx configuration:

sudo cat > /etc/nginx/sites-available/rails.conf << 'EOF'
upstream puma {
    server unix:///tmp/puma.sock fail_timeout=0;
}

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;
    root /home/rails/app/public;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    access_log /var/log/nginx/rails_access.log;
    error_log /var/log/nginx/rails_error.log;

    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;

    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_types text/plain text/css text/xml text/javascript application/x-javascript;

    # Cache static assets
    location ^~ /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        add_header ETag "";
    }

    # Cache public files
    location ^~ /public/ {
        expires 7d;
        add_header Cache-Control "public";
    }

    # Proxy to Puma
    location / {
        proxy_pass http://puma;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Host $host;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_redirect off;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }

    # Deny access to sensitive files
    location ~ /\. {
        deny all;
    }
}
EOF

sudo ln -s /etc/nginx/sites-available/rails.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Asset Pipeline

Configure Rails asset pipeline for production.

Precompile assets:

cd /home/rails/app

# Precompile CSS, JavaScript, and images
sudo -u rails bundle exec rails assets:precompile

# Verify
ls -la public/assets/

Configure asset serving:

# Add to config/environments/production.rb
cat >> /home/rails/app/config/environments/production.rb << 'EOF'
# Enable serving of images, stylesheets, and JavaScripts from an asset server
config.asset_host = nil  # Use Nginx to serve

# Compress CSS
config.assets.css_compressor = :sass

# Digest assets for cache busting
config.assets.digest = true

# Use Nginx for static asset serving
config.public_file_server.enabled = true
config.public_file_server.headers = { 'Cache-Control' => "public, max-age=#{1.year.to_i}" }
EOF

Background Job Processing

Configure Sidekiq for async job processing.

Install Redis:

sudo apt install redis-server -y
sudo systemctl start redis-server
sudo systemctl enable redis-server

Add Sidekiq gem:

# Already in Gemfile
gem 'sidekiq'

# Install
cd /home/rails/app
sudo -u rails bundle install

Configure Sidekiq:

sudo -u rails cat > /home/rails/app/config/sidekiq.yml << 'EOF'
---
:concurrency: 5
:timeout: 25
:verbose: false
:max_retries: 3

:queues:
  - default
  - mailer
  - analytics

production:
  :concurrency: 10
EOF

Create Sidekiq systemd service:

sudo cat > /etc/systemd/system/sidekiq.service << 'EOF'
[Unit]
Description=Sidekiq
After=network.target

[Service]
Type=simple
User=rails
WorkingDirectory=/home/rails/app
Environment="RAILS_ENV=production"
Environment="BUNDLE_GEMFILE=/home/rails/app/Gemfile"

ExecStart=/home/rails/.rbenv/shims/bundle exec sidekiq -c 10 -v

Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable sidekiq
sudo systemctl start sidekiq

Security and Optimization

Implement security practices and performance optimization.

Configure production secrets:

# Use Rails credential system
cd /home/rails/app
EDITOR="nano" sudo -u rails bundle exec rails credentials:edit

# Add sensitive data:
# secret_key_base: your-key
# database_password: password
# api_key: secret

Enable HTTPS enforcement:

# In config/environments/production.rb
config.force_ssl = true
config.ssl_options = { hsts: { subdomains: true, preload: true, max_age: 1.year } }

Set secure cookies:

config.session_store :cookie_store, key: '_rails_session', secure: true, http_only: true
config.action_controller.forgery_protection_origin_check = true

Configure logging:

# In config/environments/production.rb
config.log_level = :info
config.logger = Logger.new("log/production.log", 5, 100.megabytes)

Monitor performance:

# Check Puma workers
ps aux | grep puma

# Monitor database
psql -U rails_user -d rails_db -c "SELECT count(*) FROM pg_stat_activity;"

# Check logs
tail -f /home/rails/app/log/production.log
tail -f /var/log/nginx/rails_error.log

Conclusion

Deploying Rails applications with Puma and Nginx requires coordinated setup of Ruby environment, gem dependencies, database, application server, and reverse proxy. This guide covers production-ready deployment with rbenv for version management, bundler for dependency isolation, Puma for efficient request handling, Nginx for reverse proxy and static asset serving, PostgreSQL for data persistence, and Sidekiq for background jobs. Key focus areas are proper environment setup ensuring correct Ruby version, secure credential management, asset precompilation for efficient delivery, and systemd service management for reliability. Regular monitoring of application and system logs ensures continued operation. Following these practices creates a robust Rails deployment ready for production traffic and scalability.