Ruby on Rails powers over 3.8 million websites and is the backbone of industry giants like GitHub, Shopify, Basecamp, and Airbnb. With Rails 7 introducing modern features like Hotwire, importmaps, and enhanced performance, it’s never been better for building scalable web applications. Deploying Rails on Ubuntu servers provides complete infrastructure control while maintaining Rails’ powerful features. This comprehensive guide shows you how to deploy, optimize, and scale Ruby on Rails applications on Ubuntu servers for production in 2025.

Note: This guide focuses on deploying Ruby on Rails on Ubuntu servers. CloudPloy currently supports Laravel applications, with Ruby on Rails support coming soon. Stay tuned for updates!

Why Ruby on Rails Excels in Production

Rails has proven its enterprise-readiness through years of powering high-traffic applications:

  • Convention over Configuration: Rapid development with sensible defaults
  • Full-stack framework: Everything needed for web applications
  • Mature ecosystem: 170,000+ gems for any functionality
  • Developer productivity: Build features 10x faster than other frameworks
  • Scalability proven: GitHub serves 100+ million developers
  • Security-first: Built-in protection against common vulnerabilities

Modern Rails 7 Production Architecture

Rails 7 Application Structure

# Gemfile - Production-optimized dependencies
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '3.2.2'

# Core Rails gems
gem 'rails', '~> 7.1.0'
gem 'pg', '~> 1.5' # PostgreSQL adapter
gem 'puma', '~> 6.0' # Web server
gem 'redis', '~> 5.0' # Key-value store
gem 'bootsnap', require: false # Reduces boot times

# Asset pipeline and frontend
gem 'sprockets-rails' # Asset pipeline
gem 'importmap-rails' # ES6 modules
gem 'turbo-rails' # Hotwire Turbo
gem 'stimulus-rails' # Hotwire Stimulus
gem 'cssbundling-rails' # CSS bundling
gem 'jsbundling-rails' # JS bundling

# Authentication and authorization
gem 'devise' # Authentication
gem 'omniauth' # Multi-provider authentication
gem 'omniauth-rails_csrf_protection'
gem 'cancancan' # Authorization
gem 'rolify' # Role management

# Background jobs and caching
gem 'sidekiq', '~> 7.0' # Background processing
gem 'sidekiq-cron' # Scheduled jobs
gem 'dalli' # Memcached client
gem 'redis-namespace' # Redis namespacing

# Performance and monitoring
gem 'rack-mini-profiler' # Performance profiling
gem 'memory_profiler' # Memory analysis
gem 'stackprof' # CPU profiling
gem 'newrelic_rpm' # APM monitoring
gem 'sentry-ruby' # Error tracking
gem 'sentry-rails'

# Security
gem 'brakeman' # Security scanning
gem 'bundler-audit' # Gem vulnerability scanning
gem 'rack-attack' # Rate limiting
gem 'secure_headers' # Security headers

# API and serialization
gem 'jsonapi-serializer' # Fast JSON API serialization
gem 'oj' # Optimized JSON
gem 'multi_json' # Multiple JSON backends

# Database and search
gem 'searchkick' # Elasticsearch
gem 'kaminari' # Pagination
gem 'groupdate' # Time-based grouping
gem 'chartkick' # Charts

# File uploads and processing
gem 'image_processing', '~> 1.2' # Active Storage variants
gem 'aws-sdk-s3' # S3 integration

# Utilities
gem 'chronic' # Natural language dates
gem 'friendly_id' # URL slugs
gem 'paranoia' # Soft deletes
gem 'paper_trail' # Auditing

group :development, :test do
  gem 'debug', platforms: %i[ mri mingw x64_mingw ]
  gem 'factory_bot_rails' # Test data generation
  gem 'faker' # Fake data
  gem 'rspec-rails' # Testing framework
  gem 'shoulda-matchers' # Test matchers
  gem 'database_cleaner-active_record'
  gem 'vcr' # HTTP interaction recording
  gem 'webmock' # HTTP request stubbing
end

group :development do
  gem 'web-console' # Console in browser
  gem 'listen' # File watcher
  gem 'spring' # Application preloader
  gem 'spring-watcher-listen'
  gem 'letter_opener' # Email preview
  gem 'annotate' # Model annotations
  gem 'bullet' # N+1 query detection
  gem 'rubocop', require: false # Code linting
  gem 'rubocop-rails', require: false
  gem 'rubocop-performance', require: false
end

group :production do
  gem 'lograge' # Structured logging
  gem 'concurrent-ruby', require: false # Concurrency utilities
end

Production Environment Configuration

# config/environments/production.rb - Optimized production settings
require "active_support/core_ext/integer/time"

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # Code is not reloaded between requests.
  config.cache_classes = true

  # Eager load code on boot for better performance and memory usage.
  config.eager_load = true

  # Full error reports are disabled and caching is turned on.
  config.consider_all_requests_local       = false
  config.action_controller.perform_caching = true

  # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"]
  config.require_master_key = true

  # Disable serving static files from the `/public` folder by default since
  # Apache or NGINX already handles this.
  config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?

  # Compress CSS using a preprocessor.
  config.assets.css_compressor = :sass

  # Do not fallback to assets pipeline if a precompiled asset is missed.
  config.assets.compile = false

  # Enable serving of images, stylesheets, and JavaScripts from an asset server.
  config.asset_host = ENV["ASSET_HOST"]

  # Specifies the header that your server uses for sending files.
  config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
  # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX

  # Store uploaded files on the remote server.
  config.active_storage.variant_processor = :image_processing

  # Mount Action Cable outside main process or domain.
  config.action_cable.mount_path = nil
  config.action_cable.url = "ws://#{ENV['DOMAIN']}/cable"
  config.action_cable.allowed_request_origins = [ 
    /http:\/\/#{ENV['DOMAIN']}/,
    /https:\/\/#{ENV['DOMAIN']}/
  ]

  # Force all access to the app over SSL
  config.force_ssl = true

  # Log to STDOUT by default
  config.logger = ActiveSupport::Logger.new(STDOUT)
    .tap  { |logger| logger.formatter = ::Logger::Formatter.new }
    .then { |logger| ActiveSupport::TaggedLogging.new(logger) }

  # Prepend all log lines with the following tags.
  config.log_tags = [ :request_id ]

  # Use a different cache store in production.
  config.cache_store = :redis_cache_store, {
    url: ENV['REDIS_CACHE_URL'],
    namespace: 'myapp_cache',
    pool_size: ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i,
    pool_timeout: 5,
    reconnect_attempts: 3,
    error_handler: -> (method:, returning:, exception:) {
      Rails.logger.error("Redis cache error: #{exception}")
      Sentry.capture_exception(exception) if defined?(Sentry)
    }
  }

  # Use a real queuing backend for Active Job
  config.active_job.queue_adapter = :sidekiq
  config.active_job.queue_name_prefix = "myapp_production"

  # Ignore bad email addresses and do not raise email delivery errors.
  config.action_mailer.raise_delivery_errors = true
  config.action_mailer.perform_caching = false
  config.action_mailer.default_url_options = { 
    host: ENV['DOMAIN'], 
    protocol: 'https' 
  }
  
  # SMTP configuration
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = {
    address:              ENV['SMTP_HOST'],
    port:                 ENV['SMTP_PORT'],
    domain:               ENV['DOMAIN'],
    user_name:            ENV['SMTP_USERNAME'],
    password:             ENV['SMTP_PASSWORD'],
    authentication:       'plain',
    enable_starttls_auto: true,
    open_timeout:         10,
    read_timeout:         10
  }

  # Enable locale fallbacks for I18n
  config.i18n.fallbacks = true

  # Don't log any deprecations.
  config.active_support.report_deprecations = false

  # Use default logging formatter so that PID and timestamp are not suppressed.
  config.log_formatter = ::Logger::Formatter.new

  # Use a different logger for distributed setups.
  if ENV["RAILS_LOG_TO_STDOUT"].present?
    logger           = ActiveSupport::Logger.new(STDOUT)
    logger.formatter = config.log_formatter
    config.logger    = ActiveSupport::TaggedLogging.new(logger)
  end

  # Do not dump schema after migrations.
  config.active_record.dump_schema_after_migration = false

  # Inserts middleware to perform automatic connection switching.
  config.active_record.database_selector = { delay: 2.seconds }
  config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
  config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session

  # Database connection pool configuration
  config.database_configuration[Rails.env]["pool"] = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i
  config.database_configuration[Rails.env]["checkout_timeout"] = 5
  config.database_configuration[Rails.env]["reaping_frequency"] = 10

  # Session configuration
  config.session_store :redis_store,
    servers: [ENV['REDIS_SESSION_URL']],
    expire_after: 2.weeks,
    key: '_myapp_session',
    threadsafe: true,
    secure: true,
    httponly: true,
    same_site: :lax

  # Security headers
  config.force_ssl = true
  config.ssl_options = {
    hsts: {
      expires: 1.year,
      subdomains: true,
      preload: true
    }
  }
end

Database Configuration with Connection Pooling

# config/database.yml - Production database configuration
default: &default
  adapter: postgresql
  encoding: unicode
  username: <%= ENV['DATABASE_USERNAME'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>
  host: <%= ENV['DATABASE_HOST'] %>
  port: <%= ENV.fetch('DATABASE_PORT', 5432) %>
  pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
  timeout: 5000
  checkout_timeout: 5
  reaping_frequency: 10
  variables:
    statement_timeout: 30000
    lock_timeout: 10000
    idle_in_transaction_session_timeout: 60000
  
production:
  <<: *default
  database: <%= ENV['DATABASE_NAME'] %>
  # Connection pooling for high concurrency
  pool: <%= ENV.fetch("DATABASE_POOL_SIZE", 25) %>
  # Enable query plan caching
  prepared_statements: true
  # Log slow queries (>500ms)
  min_messages: warning
  # SSL configuration
  sslmode: require
  sslcert: <%= ENV['DATABASE_SSL_CERT'] if ENV['DATABASE_SSL_CERT'] %>
  sslkey: <%= ENV['DATABASE_SSL_KEY'] if ENV['DATABASE_SSL_KEY'] %>
  sslrootcert: <%= ENV['DATABASE_SSL_ROOT_CERT'] if ENV['DATABASE_SSL_ROOT_CERT'] %>

# Read replica configuration
production_replica:
  <<: *default
  database: <%= ENV['DATABASE_NAME'] %>
  host: <%= ENV['DATABASE_REPLICA_HOST'] %>
  username: <%= ENV['DATABASE_REPLICA_USERNAME'] %>
  password: <%= ENV['DATABASE_REPLICA_PASSWORD'] %>
  replica: true

Puma Web Server Configuration

Production Puma Setup

# config/puma.rb - Production-optimized Puma configuration
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 timeout for long-running requests
worker_timeout 30

# Preload the application before forking worker processes
preload_app!

# Number of worker processes
workers ENV.fetch("WEB_CONCURRENCY", 2)

# Set up socket location
bind_uri = ENV.fetch("BIND_URI", "unix:///tmp/puma.sock")
bind bind_uri

# Specify the environment
environment ENV.fetch("RAILS_ENV", "development")

# Specify the PID file
pidfile ENV.fetch("PIDFILE", "tmp/pids/puma.pid")

# Specify the state file
state_path ENV.fetch("PUMA_STATE_PATH", "tmp/pids/puma.state")

# Redirect STDOUT and STDERR to files
stdout_redirect ENV.fetch("PUMA_STDOUT", "log/puma_access.log"), 
               ENV.fetch("PUMA_STDERR", "log/puma_error.log"), true

# Configure SSL if needed
if ENV['PUMA_SSL_CERT'] && ENV['PUMA_SSL_KEY']
  ssl_bind "0.0.0.0", ENV.fetch("PUMA_SSL_PORT", 443),
    cert: ENV['PUMA_SSL_CERT'],
    key: ENV['PUMA_SSL_KEY'],
    verify_mode: "none"
end

# Worker killer to prevent memory leaks
if ENV['RAILS_ENV'] == 'production'
  before_fork do
    require 'puma_worker_killer'
    
    PumaWorkerKiller.config do |config|
      config.ram = ENV.fetch("PUMA_WORKER_KILLER_RAM", 1024) # MB
      config.frequency = ENV.fetch("PUMA_WORKER_KILLER_FREQUENCY", 10) # seconds
      config.percent_usage = ENV.fetch("PUMA_WORKER_KILLER_PERCENT", 0.98)
      config.rolling_restart_frequency = 12 * 3600 # 12 hours
    end
    
    PumaWorkerKiller.start
  end
end

# Before forking, disconnect from database
on_worker_boot do
  ActiveSupport.on_load(:active_record) do
    ActiveRecord::Base.establish_connection
  end
end

# Allow puma to be restarted by 'rails restart' command
plugin :tmp_restart

# Health check endpoint
activate_control_app "unix://tmp/puma_control.sock"

# Low-level socket options
tcp_mode!
backlog ENV.fetch("PUMA_BACKLOG", 1024).to_i

# Graceful shutdown
on_worker_shutdown do
  puts "Worker #{Process.pid} shutting down gracefully..."
end

# Memory and CPU monitoring
if ENV['RAILS_ENV'] == 'production'
  lowlevel_error_handler do |ex, env|
    Sentry.capture_exception(ex) if defined?(Sentry)
    
    [500, {}, ["An error occurred processing your request"]]
  end
end

# Tag workers with their index for easier debugging
tag 'rails-app'

Advanced Caching Strategies

Multi-layer Caching Implementation

# app/models/concerns/cacheable.rb - Caching concern
module Cacheable
  extend ActiveSupport::Concern

  included do
    after_update :clear_cache
    after_destroy :clear_cache
  end

  class_methods do
    def cached_find(id, expires_in: 1.hour)
      Rails.cache.fetch(cache_key_for_find(id), expires_in: expires_in) do
        find(id)
      end
    end

    def cached_where(conditions, expires_in: 30.minutes)
      cache_key = "#{name.downcase}/where/#{Digest::MD5.hexdigest(conditions.to_s)}"
      Rails.cache.fetch(cache_key, expires_in: expires_in) do
        where(conditions).to_a
      end
    end

    def cached_count(conditions = {}, expires_in: 1.hour)
      cache_key = "#{name.downcase}/count/#{Digest::MD5.hexdigest(conditions.to_s)}"
      Rails.cache.fetch(cache_key, expires_in: expires_in) do
        where(conditions).count
      end
    end

    private

    def cache_key_for_find(id)
      "#{name.downcase}/#{id}"
    end
  end

  private

  def clear_cache
    # Clear individual record cache
    Rails.cache.delete("#{self.class.name.downcase}/#{id}")
    
    # Clear collection caches
    clear_collection_cache
    
    # Clear associated model caches
    clear_associated_caches
  end

  def clear_collection_cache
    # Clear all where and count caches for this model
    pattern = "#{self.class.name.downcase}/{where,count}/*"
    delete_matched_cache(pattern)
  end

  def clear_associated_caches
    # Override in models to clear related caches
  end

  def delete_matched_cache(pattern)
    if Rails.cache.respond_to?(:delete_matched)
      Rails.cache.delete_matched(pattern)
    else
      # For Redis cache stores that don't support pattern deletion
      RedisCache.new.delete_matched(pattern)
    end
  end
end

# Advanced cache service
class CacheService
  include Singleton

  def initialize
    @redis = Redis.new(url: ENV['REDIS_CACHE_URL'])
  end

  # Multi-get for batch cache operations
  def multi_get(keys)
    results = {}
    cache_keys = keys.map { |key| cache_key(key) }
    
    cached_values = Rails.cache.read_multi(*cache_keys)
    
    keys.each_with_index do |key, index|
      cache_key_val = cache_keys[index]
      results[key] = cached_values[cache_key_val]
    end
    
    results
  end

  # Cache with automatic serialization
  def set_json(key, value, expires_in: 1.hour)
    Rails.cache.write(
      cache_key(key),
      value.to_json,
      expires_in: expires_in,
      compress: value.to_json.bytesize > 1024
    )
  end

  def get_json(key)
    cached = Rails.cache.read(cache_key(key))
    cached ? JSON.parse(cached) : nil
  rescue JSON::ParserError
    nil
  end

  # Fragment caching with dependencies
  def cache_with_dependencies(key, dependencies: [], expires_in: 1.hour, &block)
    cache_key_with_deps = "#{cache_key(key)}/#{dependency_hash(dependencies)}"
    
    Rails.cache.fetch(cache_key_with_deps, expires_in: expires_in) do
      yield
    end
  end

  # Warming cache in background
  def warm_cache(key, expires_in: 1.hour, &block)
    WarmCacheJob.perform_async(key, expires_in, &block)
  end

  # Cache tagging for group invalidation
  def tagged_cache(tags, key, expires_in: 1.hour, &block)
    Rails.cache.fetch(
      cache_key(key),
      expires_in: expires_in,
      tags: Array(tags)
    ) do
      yield
    end
  end

  def invalidate_tag(tag)
    Rails.cache.delete_matched("*[#{tag}]*")
  end

  private

  def cache_key(key)
    "#{Rails.application.class.module_parent.name.underscore}/#{Rails.env}/#{key}"
  end

  def dependency_hash(dependencies)
    Digest::MD5.hexdigest(dependencies.map(&:cache_key).join('/'))
  end
end

# Usage in models
class Article < ApplicationRecord
  include Cacheable

  has_many :comments
  belongs_to :author, class_name: 'User'

  def self.popular_articles(limit: 10)
    CacheService.instance.cache_with_dependencies(
      "articles/popular/#{limit}",
      dependencies: [Article.maximum(:updated_at), Comment.maximum(:created_at)],
      expires_in: 2.hours
    ) do
      joins(:comments)
        .group('articles.id')
        .order('COUNT(comments.id) DESC')
        .limit(limit)
        .includes(:author)
    end
  end

  def related_articles(limit: 5)
    CacheService.instance.tagged_cache(
      ["articles", "user_#{author_id}"],
      "articles/#{id}/related/#{limit}",
      expires_in: 4.hours
    ) do
      Article.where(category: category)
             .where.not(id: id)
             .limit(limit)
             .includes(:author)
    end
  end

  private

  def clear_associated_caches
    # Clear author's article cache
    CacheService.instance.invalidate_tag("user_#{author_id}")
    
    # Clear popular articles cache
    Rails.cache.delete_matched("articles/popular/*")
  end
end

Background Jobs with Sidekiq

Sidekiq Configuration and Job Processing

# config/initializers/sidekiq.rb - Production Sidekiq setup
require 'sidekiq'
require 'sidekiq-cron'

Sidekiq.configure_server do |config|
  config.redis = {
    url: ENV['REDIS_SIDEKIQ_URL'],
    namespace: 'myapp_sidekiq',
    network_timeout: 5,
    pool_timeout: 5,
    size: ENV.fetch('SIDEKIQ_REDIS_POOL_SIZE', 25).to_i
  }

  # Database connection pool for background jobs
  config.options[:concurrency] = ENV.fetch('SIDEKIQ_CONCURRENCY', 10).to_i
  
  ActiveRecord::Base.configurations[Rails.env]['pool'] = 
    ENV.fetch('SIDEKIQ_DATABASE_POOL_SIZE', 25).to_i

  # Error handling
  config.error_handlers << proc { |ex, ctx_hash|
    Sentry.capture_exception(ex, extra: ctx_hash) if defined?(Sentry)
    Rails.logger.error "Sidekiq error: #{ex.message}"
  }

  # Death handlers for failed jobs
  config.death_handlers << proc { |job, ex|
    DeadJobNotifier.new(job, ex).notify
  }

  # Periodic job cleanup
  config.periodic do |mgr|
    mgr.register('0 2 * * *', 'CleanupJob') # Daily at 2 AM
  end

  # Lifecycle callbacks
  config.on :startup do
    puts "Sidekiq starting with #{config.options[:concurrency]} threads"
  end

  config.on :shutdown do
    puts "Sidekiq shutting down gracefully"
  end
end

Sidekiq.configure_client do |config|
  config.redis = {
    url: ENV['REDIS_SIDEKIQ_URL'],
    namespace: 'myapp_sidekiq',
    network_timeout: 5,
    pool_timeout: 5,
    size: ENV.fetch('SIDEKIQ_CLIENT_POOL_SIZE', 5).to_i
  }
end

# Load cron jobs from configuration
if Sidekiq.server?
  schedule_file = Rails.root.join('config', 'schedule.yml')
  if File.exist?(schedule_file)
    Sidekiq::Cron::Job.load_from_hash YAML.load_file(schedule_file)
  end
end

# Middleware for job tracking
class JobTrackingMiddleware
  def call(worker, msg, queue)
    start_time = Time.now
    job_id = msg['jid']
    
    Rails.logger.info "Starting job #{job_id} (#{worker.class.name}) on queue #{queue}"
    
    yield
    
    duration = Time.now - start_time
    Rails.logger.info "Completed job #{job_id} in #{duration.round(2)}s"
  rescue => ex
    duration = Time.now - start_time
    Rails.logger.error "Failed job #{job_id} after #{duration.round(2)}s: #{ex.message}"
    raise ex
  end
end

Sidekiq.configure_server do |config|
  config.server_middleware do |chain|
    chain.add JobTrackingMiddleware
  end
end

Advanced Background Job Classes

# app/jobs/application_job.rb - Base job class
class ApplicationJob < ActiveJob::Base
  include Sidekiq::Throttled::Job
  
  # Automatically retry jobs with exponential backoff
  retry_on StandardError, wait: :exponentially_longer, attempts: 5
  retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
  retry_on Redis::TimeoutError, wait: 1.second, attempts: 3

  # Don't retry certain errors
  discard_on ActiveJob::DeserializationError
  discard_on ActionController::RoutingError

  # Global callbacks
  before_enqueue do |job|
    Rails.logger.info "Enqueuing job: #{job.class.name} with args: #{job.arguments}"
  end

  before_perform do |job|
    Rails.logger.info "Starting job: #{job.class.name} (#{job.job_id})"
  end

  after_perform do |job|
    Rails.logger.info "Completed job: #{job.class.name} (#{job.job_id})"
  end

  around_perform do |job, block|
    Benchmark.realtime do
      block.call
    end.tap do |time|
      Rails.logger.info "Job #{job.class.name} took #{time.round(2)} seconds"
    end
  end

  private

  def with_error_handling(&block)
    yield
  rescue => error
    handle_job_error(error)
    raise error
  end

  def handle_job_error(error)
    # Custom error handling logic
    ErrorTracker.capture_exception(error, {
      job_class: self.class.name,
      job_id: job_id,
      arguments: arguments
    })
  end
end

# Heavy processing job with progress tracking
class DataProcessingJob < ApplicationJob
  include Sidekiq::Status::Worker
  
  queue_as :heavy_processing
  sidekiq_options retry: 3, backtrace: true
  
  # Rate limiting: max 5 jobs per minute
  sidekiq_throttle threshold: { limit: 5, period: 1.minute }

  def perform(dataset_id, options = {})
    dataset = Dataset.find(dataset_id)
    total_records = dataset.records.count
    
    at(0, total_records, "Starting processing...")
    
    dataset.records.find_each.with_index do |record, index|
      # Check if job was cancelled
      return if cancelled?
      
      process_record(record, options)
      
      # Update progress every 100 records
      if (index + 1) % 100 == 0
        progress = ((index + 1.0) / total_records * 100).round(2)
        at(index + 1, total_records, "Processed #{index + 1} records (#{progress}%)")
      end
    end
    
    # Mark dataset as processed
    dataset.update!(status: 'processed', processed_at: Time.current)
    
    # Send completion notification
    DataProcessingMailer.processing_complete(dataset).deliver_now
    
    at(total_records, total_records, "Processing completed!")
  end

  private

  def process_record(record, options)
    # Simulate heavy processing
    sleep(0.1) if Rails.env.development?
    
    # Actual processing logic here
    record.update!(
      processed: true,
      processed_at: Time.current,
      processing_options: options
    )
  end

  def cancelled?
    # Check Redis for cancellation flag
    cancellation_key = "job_cancellation:#{jid}"
    Rails.cache.exist?(cancellation_key)
  end
end

# Email delivery job with improved reliability
class EmailDeliveryJob < ApplicationJob
  queue_as :mailers
  
  # Custom retry logic for email failures
  retry_on Net::SMTPServerBusy, wait: 1.minute, attempts: 10
  retry_on Net::SMTPSyntaxError, attempts: 3
  retry_on Timeout::Error, wait: 30.seconds, attempts: 5
  
  discard_on Net::SMTPFatalError
  discard_on ArgumentError

  def perform(mailer_class, mailer_method, *args)
    with_email_tracking do
      mailer_class.constantize.send(mailer_method, *args).deliver_now
    end
  end

  private

  def with_email_tracking
    start_time = Time.current
    
    yield
    
    # Track successful delivery
    EmailMetrics.increment_counter('emails_sent')
    EmailMetrics.record_delivery_time(Time.current - start_time)
    
  rescue => error
    # Track failed delivery
    EmailMetrics.increment_counter('emails_failed')
    EmailMetrics.record_error(error.class.name)
    
    raise error
  end
end

# Scheduled cleanup job
class CleanupJob < ApplicationJob
  queue_as :maintenance
  
  def perform
    Rails.logger.info "Starting scheduled cleanup tasks"
    
    cleanup_expired_sessions
    cleanup_old_logs
    cleanup_temporary_files
    optimize_database
    
    Rails.logger.info "Cleanup tasks completed"
  end

  private

  def cleanup_expired_sessions
    expired_count = ActiveRecord::SessionStore::Session
      .where("updated_at < ?", 30.days.ago)
      .delete_all
    
    Rails.logger.info "Deleted #{expired_count} expired sessions"
  end

  def cleanup_old_logs
    log_files = Dir[Rails.root.join('log', '*.log')]
    
    log_files.each do |file|
      if File.mtime(file) < 7.days.ago
        File.delete(file)
        Rails.logger.info "Deleted old log file: #{file}"
      end
    end
  end

  def cleanup_temporary_files
    temp_dir = Rails.root.join('tmp', 'cache')
    return unless Dir.exist?(temp_dir)
    
    FileUtils.rm_rf(Dir[temp_dir.join('*')])
    Rails.logger.info "Cleaned temporary cache files"
  end

  def optimize_database
    return unless Rails.env.production?
    
    ActiveRecord::Base.connection.execute('ANALYZE;')
    Rails.logger.info "Database statistics updated"
  end
end

Action Cable and WebSocket Configuration

Production WebSocket Setup

# config/cable.yml - Action Cable configuration
production:
  adapter: redis
  url: <%= ENV['REDIS_CABLE_URL'] %>
  channel_prefix: myapp_production
  timeout: 1
  reconnect_attempts: 3
  reconnect_timeout: 2
  max_message_size: 256KB

# app/channels/application_cable/connection.rb - Connection authentication
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user, :current_session_id

    def connect
      self.current_user = find_verified_user
      self.current_session_id = generate_session_id
      
      Rails.logger.info "ActionCable connected: User #{current_user.id}"
    end

    def disconnect
      Rails.logger.info "ActionCable disconnected: User #{current_user&.id}"
    end

    private

    def find_verified_user
      # Try JWT token from params
      if token = request.params[:token]
        decoded_token = JWT.decode(token, Rails.application.secret_key_base)[0]
        User.find(decoded_token['user_id'])
      # Try session cookie
      elsif session_user_id = cookies.encrypted[:user_id]
        User.find(session_user_id)
      else
        reject_unauthorized_connection
      end
    rescue JWT::DecodeError, ActiveRecord::RecordNotFound
      reject_unauthorized_connection
    end

    def generate_session_id
      SecureRandom.hex(16)
    end
  end
end

# Real-time notification channel
class NotificationsChannel < ApplicationCable::Channel
  def subscribed
    stream_for current_user
    Rails.logger.info "User #{current_user.id} subscribed to notifications"
  end

  def unsubscribed
    Rails.logger.info "User #{current_user.id} unsubscribed from notifications"
  end

  def mark_as_read(data)
    notification = current_user.notifications.find(data['id'])
    notification.update!(read_at: Time.current)
    
    broadcast_to current_user, {
      type: 'notification_read',
      id: notification.id
    }
  end
end

# Chat channel with presence
class ChatChannel < ApplicationCable::Channel
  def subscribed
    @room = Room.find(params[:room_id])
    
    # Verify user has access to room
    unless @room.accessible_by?(current_user)
      reject
      return
    end
    
    stream_for @room
    
    # Track user presence
    add_user_to_room
    broadcast_user_joined
  end

  def unsubscribed
    remove_user_from_room
    broadcast_user_left
  end

  def speak(data)
    message = @room.messages.create!(
      user: current_user,
      content: data['message'],
      message_type: data['type'] || 'text'
    )
    
    # Broadcast to all room subscribers
    ChatChannel.broadcast_to @room, {
      type: 'new_message',
      message: render_message(message),
      user: serialize_user(current_user)
    }
    
    # Send push notifications to offline users
    notify_offline_users(message)
  end

  def typing
    # Broadcast typing indicator (temporary, no persistence)
    ActionCable.server.broadcast(
      "chat_#{@room.id}_typing",
      {
        type: 'typing',
        user_id: current_user.id,
        user_name: current_user.name
      }
    )
  end

  def stop_typing
    ActionCable.server.broadcast(
      "chat_#{@room.id}_typing",
      {
        type: 'stop_typing',
        user_id: current_user.id
      }
    )
  end

  private

  def add_user_to_room
    Redis.current.sadd("room_#{@room.id}_users", current_user.id)
    Redis.current.expire("room_#{@room.id}_users", 1.hour)
  end

  def remove_user_from_room
    Redis.current.srem("room_#{@room.id}_users", current_user.id)
  end

  def broadcast_user_joined
    ChatChannel.broadcast_to @room, {
      type: 'user_joined',
      user: serialize_user(current_user),
      online_users: get_online_users
    }
  end

  def broadcast_user_left
    ChatChannel.broadcast_to @room, {
      type: 'user_left',
      user_id: current_user.id,
      online_users: get_online_users
    }
  end

  def get_online_users
    user_ids = Redis.current.smembers("room_#{@room.id}_users")
    User.where(id: user_ids).pluck(:id, :name, :avatar_url)
  end

  def render_message(message)
    ApplicationController.render(
      partial: 'messages/message',
      locals: { message: message }
    )
  end

  def serialize_user(user)
    {
      id: user.id,
      name: user.name,
      avatar_url: user.avatar_url
    }
  end

  def notify_offline_users(message)
    offline_users = @room.users.where.not(id: get_online_users.map(&:first))
    
    offline_users.each do |user|
      PushNotificationJob.perform_later(
        user.id,
        title: "New message from #{current_user.name}",
        body: message.content,
        data: { room_id: @room.id, message_id: message.id }
      )
    end
  end
end

Performance Optimization and Monitoring

Database Query Optimization

# app/models/concerns/query_optimized.rb - Query optimization concern
module QueryOptimized
  extend ActiveSupport::Concern

  included do
    # Automatic includes for common associations
    scope :with_associations, -> { includes(default_includes) if respond_to?(:default_includes) }
    
    # Efficient counting with caching
    scope :cached_count, ->(cache_key = nil, expires_in: 1.hour) {
      key = cache_key || "#{name.underscore}/count/#{all.to_sql.hash}"
      Rails.cache.fetch(key, expires_in: expires_in) { count }
    }
  end

  class_methods do
    # Batch loading to prevent N+1 queries
    def load_with_associations(*associations)
      includes(*associations).find_each do |record|
        # Preload associations into memory
        associations.each do |assoc|
          record.association(assoc).load_target
        end
        yield record
      end
    end

    # Efficient batch updates
    def batch_update(conditions, attributes)
      where(conditions).update_all(attributes.merge(updated_at: Time.current))
    end

    # Raw SQL for complex queries with safety checks
    def execute_raw_sql(sql, binds = [])
      # Validate SQL is read-only for safety
      raise ArgumentError, "Only SELECT queries allowed" unless sql.strip.match?(/\ASELECT/i)
      
      connection.exec_query(sql, "#{name} Raw SQL", binds)
    end
  end

  def touch_with_cache_invalidation(*attributes)
    # Clear relevant caches before touching
    invalidate_related_caches
    touch(*attributes)
  end

  private

  def invalidate_related_caches
    # Override in models to specify cache invalidation logic
  end
end

# Advanced query monitoring
class QueryMonitor
  def self.monitor_slow_queries
    ActiveSupport::Notifications.subscribe('sql.active_record') do |name, started, finished, unique_id, data|
      duration = finished - started
      
      if duration > 1.0 # Log queries slower than 1 second
        Rails.logger.warn "Slow Query (#{duration.round(2)}s): #{data[:sql]}"
        
        # Send to monitoring service
        if defined?(NewRelic)
          NewRelic::Agent.record_metric('Database/SlowQuery', duration)
        end
        
        # Capture in Sentry with context
        if defined?(Sentry)
          Sentry.capture_message("Slow database query", level: :warning, extra: {
            duration: duration,
            sql: data[:sql],
            binds: data[:binds]
          })
        end
      end
    end
  end

  def self.monitor_n_plus_one
    Bullet.enable = true
    Bullet.bullet_logger = true
    Bullet.console = true if Rails.env.development?
    
    # Send N+1 alerts to monitoring
    Bullet.add_footer = false
    Bullet.raise = Rails.env.test? # Raise in tests to catch issues early
    
    if Rails.env.production?
      Bullet.airbrake = false
      Bullet.bugsnag = false
      Bullet.sentry = true
    end
  end
end

# Initialize query monitoring
QueryMonitor.monitor_slow_queries
QueryMonitor.monitor_n_plus_one if Rails.env.development? || Rails.env.test?

# Example optimized model
class User < ApplicationRecord
  include QueryOptimized
  
  has_many :articles, dependent: :destroy
  has_many :comments, dependent: :destroy
  has_one :profile, dependent: :destroy

  # Define default includes for common queries
  def self.default_includes
    [:profile]
  end

  # Optimized scopes
  scope :active, -> { where(status: 'active') }
  scope :with_articles, -> { joins(:articles).distinct }
  scope :recent, -> { order(created_at: :desc) }

  # Counter cache for performance
  has_many :articles, dependent: :destroy, counter_cache: true
  has_many :comments, dependent: :destroy, counter_cache: true

  # Efficient search
  def self.search(query)
    return none if query.blank?
    
    # Use PostgreSQL full-text search
    where("to_tsvector('english', name || ' ' || email) @@ plainto_tsquery('english', ?)", query)
  end

  # Batch operations
  def self.activate_users(user_ids)
    where(id: user_ids).batch_update({}, { status: 'active', activated_at: Time.current })
  end

  private

  def invalidate_related_caches
    Rails.cache.delete("user/#{id}/profile")
    Rails.cache.delete("user/#{id}/articles_count")
  end
end

Production Security Implementation

Comprehensive Security Setup

# config/initializers/security.rb - Security configuration
Rails.application.config.to_prepare do
  # Secure headers configuration
  SecureHeaders::Configuration.default do |config|
    config.x_frame_options = "DENY"
    config.x_content_type_options = "nosniff"
    config.x_xss_protection = "1; mode=block"
    config.x_download_options = "noopen"
    config.x_permitted_cross_domain_policies = "none"
    config.referrer_policy = %w(origin-when-cross-origin strict-origin-when-cross-origin)
    
    config.hsts = {
      max_age: 31_536_000, # 1 year
      include_subdomains: true,
      preload: true
    }
    
    config.csp = {
      preserve_schemes: true,
      default_src: %w('self'),
      font_src: %w('self' data: fonts.gstatic.com),
      img_src: %w('self' data: *.amazonaws.com),
      object_src: %w('none'),
      script_src: %w('self' 'unsafe-inline'),
      style_src: %w('self' 'unsafe-inline' fonts.googleapis.com),
      base_uri: %w('self'),
      form_action: %w('self'),
      connect_src: %w('self' wss:),
      upgrade_insecure_requests: true
    }
  end
end

# Rate limiting with Rack::Attack
class Rack::Attack
  # Allowlist localhost
  Rack::Attack.safelist('allow-localhost') do |req|
    '127.0.0.1' == req.ip || '::1' == req.ip
  end

  # Block requests from specific IPs
  blocklist_ips = ENV['BLOCKED_IPS']&.split(',') || []
  Rack::Attack.blocklist('block-ips') do |req|
    blocklist_ips.include?(req.ip)
  end

  # Throttle requests by IP
  Rack::Attack.throttle('requests by ip', limit: 300, period: 5.minutes) do |request|
    request.ip
  end

  # Throttle login attempts by IP
  Rack::Attack.throttle('login attempts by ip', limit: 5, period: 20.minutes) do |request|
    request.ip if request.path == '/users/sign_in' && request.post?
  end

  # Throttle login attempts by email
  Rack::Attack.throttle('login attempts by email', limit: 5, period: 20.minutes) do |request|
    if request.path == '/users/sign_in' && request.post?
      req_params = request.params
      req_params['user']['email'].to_s.downcase.gsub(/\s+/, '') if req_params['user']
    end
  end

  # API rate limiting
  Rack::Attack.throttle('api requests', limit: 1000, period: 1.hour) do |request|
    request.ip if request.path.start_with?('/api/')
  end

  # Heavy endpoints protection
  Rack::Attack.throttle('heavy endpoints', limit: 10, period: 1.minute) do |request|
    request.ip if ['/api/reports', '/api/exports'].any? { |path| request.path.start_with?(path) }
  end

  # Exponential backoff for repeat offenders
  (1..10).each do |level|
    Rack::Attack.blocklist("fail2ban penaltybox level #{level}") do |req|
      Rack::Attack::Fail2Ban.filter("penaltybox-#{req.ip}", maxretry: 10, findtime: 10.minutes, bantime: (2 ** level).minutes) do
        CGI.unescape(req.query_string) =~ /%2e%2e%2f/i ||
        req.path.include?('../') ||
        req.path.include?('..\\')
      end
    end
  end

  # Handle rate limit responses
  self.throttled_response_retry_after_header = true
  
  self.throttled_response = lambda do |env|
    retry_after = env['rack.attack.match_data'][:period]
    [
      429,
      {
        'Content-Type' => 'application/json',
        'Retry-After' => retry_after.to_s
      },
      [{ error: 'Rate limit exceeded', retry_after: retry_after }.to_json]
    ]
  end

  # Logging
  ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, request_id, req|
    Rails.logger.info "Rack::Attack: #{req.env['rack.attack.match_type']} #{req.ip} #{req.request_method} #{req.fullpath}"
    
    # Send alerts for blocked requests
    if req.env['rack.attack.match_type'] == :blocklist
      SecurityAlertMailer.blocked_request(req.ip, req.fullpath).deliver_later
    end
  end
end

# CSRF protection
Rails.application.config.force_ssl = true
Rails.application.config.session_store :cookie_store, 
  key: '_myapp_session',
  secure: Rails.env.production?,
  httponly: true,
  same_site: :lax

# Parameter filtering
Rails.application.config.filter_parameters += [
  :password, :password_confirmation, :token, :secret, :key, :credit_card_number
]

Container Deployment with Docker

Production Dockerfile

# syntax=docker/dockerfile:1
FROM ruby:3.2.2-alpine AS base

# Install system dependencies
RUN apk add --no-cache \
    build-base \
    postgresql-dev \
    imagemagick-dev \
    tzdata \
    curl \
    nodejs \
    npm \
    git \
    && rm -rf /var/cache/apk/*

# Set working directory
WORKDIR /rails

# Install specific Node.js version if needed
RUN npm install -g yarn

# Development stage
FROM base AS development
COPY Gemfile* ./
RUN bundle install
COPY . .
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]

# Build stage
FROM base AS build

# Copy and install Ruby dependencies
COPY Gemfile* ./
RUN bundle config set --local deployment 'true' && \
    bundle config set --local without 'development test' && \
    bundle install && \
    bundle clean --force && \
    rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git

# Copy application code
COPY . .

# Precompile assets
ENV RAILS_ENV=production
ENV SECRET_KEY_BASE=dummy_key_for_assets
RUN rails assets:precompile

# Remove unnecessary files
RUN rm -rf \
    .git \
    tmp/cache \
    spec \
    test \
    node_modules \
    app/assets \
    lib/assets \
    vendor/assets \
    .DS_Store

# Production stage
FROM ruby:3.2.2-alpine AS production

# Install runtime dependencies
RUN apk add --no-cache \
    postgresql-client \
    imagemagick \
    tzdata \
    curl \
    dumb-init \
    && addgroup -g 1001 -S rails \
    && adduser -S rails -u 1001 -G rails

# Set working directory
WORKDIR /rails

# Copy built application from build stage
COPY --from=build --chown=rails:rails /rails /rails

# Copy built gems from build stage
COPY --from=build /usr/local/bundle /usr/local/bundle

# Switch to non-root user
USER rails

# Expose port
EXPOSE 3000

# Add health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

# Set environment variables
ENV RAILS_ENV=production \
    RAILS_SERVE_STATIC_FILES=true \
    RAILS_LOG_TO_STDOUT=true \
    BUNDLE_APP_CONFIG=/usr/local/bundle

# Use dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]

# Start Puma server
CMD ["rails", "server", "-b", "0.0.0.0", "-p", "3000"]

Docker Compose for Production

version: '3.8'

services:
  web:
    build: 
      context: .
      target: production
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - RAILS_ENV=production
      - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
      - REDIS_CACHE_URL=redis://redis:6379/0
      - REDIS_SIDEKIQ_URL=redis://redis:6379/1
      - REDIS_CABLE_URL=redis://redis:6379/2
      - SECRET_KEY_BASE=${SECRET_KEY_BASE}
      - RAILS_MASTER_KEY=${RAILS_MASTER_KEY}
    volumes:
      - storage_data:/rails/storage
      - log_data:/rails/log
    depends_on:
      - db
      - redis
    networks:
      - app-network
    deploy:
      resources:
        limits:
          memory: 1G
          cpus: '0.5'
        reservations:
          memory: 512M
          cpus: '0.25'
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  sidekiq:
    build: 
      context: .
      target: production
    restart: unless-stopped
    command: bundle exec sidekiq -C config/sidekiq.yml
    environment:
      - RAILS_ENV=production
      - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
      - REDIS_SIDEKIQ_URL=redis://redis:6379/1
      - SECRET_KEY_BASE=${SECRET_KEY_BASE}
      - RAILS_MASTER_KEY=${RAILS_MASTER_KEY}
    volumes:
      - log_data:/rails/log
    depends_on:
      - db
      - redis
    networks:
      - app-network
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'
        reservations:
          memory: 256M
          cpus: '0.25'

  db:
    image: postgres:15-alpine
    restart: unless-stopped
    environment:
      - POSTGRES_DB=${POSTGRES_DB}
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - PGDATA=/var/lib/postgresql/data/pgdata
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    networks:
      - app-network
    deploy:
      resources:
        limits:
          memory: 1G
          cpus: '0.5'
    command: >
      postgres
      -c max_connections=200
      -c shared_buffers=256MB
      -c effective_cache_size=1GB
      -c maintenance_work_mem=64MB
      -c checkpoint_completion_target=0.7
      -c wal_buffers=16MB
      -c default_statistics_target=100
      -c random_page_cost=1.1
      -c effective_io_concurrency=200

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: >
      redis-server 
      --maxmemory 256mb 
      --maxmemory-policy allkeys-lru 
      --appendonly yes 
      --save 900 1 
      --save 300 10 
      --save 60 10000
    volumes:
      - redis_data:/data
    ports:
      - "6379:6379"
    networks:
      - app-network
    deploy:
      resources:
        limits:
          memory: 256M
          cpus: '0.25'
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 30s
      timeout: 10s
      retries: 3

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
      - nginx_logs:/var/log/nginx
    depends_on:
      - web
    networks:
      - app-network
    deploy:
      resources:
        limits:
          memory: 128M
          cpus: '0.25'

volumes:
  postgres_data:
  redis_data:
  storage_data:
  log_data:
  nginx_logs:

networks:
  app-network:
    driver: bridge

CloudPloy Deployment

CloudPloy simplifies Rails deployment with automatic optimization and best practices:

# cloudploy.yml - Rails deployment configuration
name: rails-app
framework: rails
version: "1.0.0"

build:
  ruby_version: "3.2.2"
  bundler_version: "2.4.0"
  build_command: |
    bundle install --deployment --without development test
    bundle exec rails assets:precompile
    bundle exec rails db:migrate
  environment:
    - RAILS_ENV=production
    - NODE_ENV=production

run:
  web:
    command: bundle exec puma -C config/puma.rb
    port: 3000
    instances: 2
  worker:
    command: bundle exec sidekiq -C config/sidekiq.yml
    instances: 1

services:
  - name: postgresql
    version: "15"
    config:
      max_connections: 200
      shared_buffers: "256MB"
      storage: 20GB
  - name: redis  
    version: "7"
    config:
      maxmemory: "256MB"
      maxmemory_policy: "allkeys-lru"

scaling:
  web:
    min_instances: 2
    max_instances: 10
    target_cpu: 70
    target_memory: 80
  worker:
    min_instances: 1
    max_instances: 5
    target_cpu: 80
    queue_size_threshold: 100

load_balancer:
  algorithm: least_conn
  health_check:
    path: /health
    interval: 30
    timeout: 10
    healthy_threshold: 2
    unhealthy_threshold: 3
  session_affinity: false

security:
  firewall:
    - port: 80
      protocol: tcp
      source: 0.0.0.0/0
    - port: 443
      protocol: tcp
      source: 0.0.0.0/0
  ssl:
    auto_renew: true
    force_https: true
  waf:
    enable: true
    rules:
      - sql_injection
      - xss_protection
      - rate_limiting

caching:
  redis:
    url: ${REDIS_CACHE_URL}
    namespace: "myapp_cache"
    ttl: 3600
  cdn:
    enable: true
    cache_static_assets: true
    cache_headers:
      - "Cache-Control: public, max-age=31536000"

monitoring:
  apm: true
  metrics:
    - response_time
    - throughput
    - error_rate
    - memory_usage
    - cpu_usage
    - database_connections
    - queue_size
  alerts:
    - condition: error_rate > 5%
      notification: email
    - condition: response_time > 1000ms
      notification: slack
    - condition: queue_size > 1000
      notification: pagerduty

environment:
  - RAILS_ENV=production
  - RACK_ENV=production
  - SECRET_KEY_BASE=${SECRET_KEY_BASE}
  - DATABASE_URL=${DATABASE_URL}
  - REDIS_CACHE_URL=${REDIS_CACHE_URL}
  - REDIS_SIDEKIQ_URL=${REDIS_SIDEKIQ_URL}
  - REDIS_CABLE_URL=${REDIS_CABLE_URL}
  - SMTP_HOST=${SMTP_HOST}
  - SMTP_USERNAME=${SMTP_USERNAME}
  - SMTP_PASSWORD=${SMTP_PASSWORD}

domains:
  - app.yourdomain.com
  - www.yourdomain.com

backup:
  databases:
    - postgresql
  files:
    - storage
  schedule: "0 2 * * *" # Daily at 2 AM
  retention: 30 # days

logs:
  retention: 30 # days
  level: info
  structured: true

auto_deploy:
  branch: main
  trigger: push
  run_tests: true
  zero_downtime: true

Performance Benchmarks

Ruby on Rails Performance Metrics

MetricSingle ProcessClustered (4 processes)Notes
Requests/sec3,50012,000Simple JSON API response
Latency (p95)45ms35msWith database queries
Latency (p99)120ms85msIncluding authentication
Memory Usage180MB650MBFull Rails stack
Startup Time8s12sWith all gems loaded
Concurrent Users1,0004,000+Active sessions
Database Connections520Per process pool

Optimization Checklist

  • ✅ Enable database connection pooling
  • ✅ Configure multi-level caching strategy
  • ✅ Implement background job processing
  • ✅ Use Puma with clustering
  • ✅ Optimize database queries and indexes
  • ✅ Enable asset compilation and compression
  • ✅ Configure Action Cable for real-time features
  • ✅ Implement comprehensive monitoring
  • ✅ Set up proper logging and error tracking
  • ✅ Configure security headers and rate limiting

Conclusion

Ruby on Rails continues to be one of the most productive frameworks for building scalable web applications. With Rails 7’s modern features like Hotwire, importmaps, and improved performance, combined with proper deployment practices, Rails applications can handle significant traffic while maintaining developer productivity.

The key to successful Rails deployment lies in understanding the full ecosystem: from database optimization and caching strategies to background job processing and real-time features. Modern deployment platforms like CloudPloy handle much of the infrastructure complexity while allowing developers to focus on building great applications.

Whether you’re building e-commerce platforms, social applications, or enterprise software, Rails provides the tools and conventions you need to ship quickly and scale effectively. Combined with proper monitoring, security, and deployment practices, your Rails applications can reliably serve millions of users.

Ready to deploy your Rails application with production-grade infrastructure? Start with CloudPloy’s managed Rails hosting today.

Deploy Rails with CloudPloy →