Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Rails Ubuntu
GUIDEInstall Guides

How to Install Ruby on Rails on Ubuntu 24.04 VPS: Production Deploy Guide

24 min read

How to Install Ruby on Rails on Ubuntu 24.04 VPS: Production Deploy Guide

Deploying a Ruby on Rails application on your own VPS gives you complete control over the stack, predictable costs, and the freedom to tune every layer — from the Ruby version manager to the reverse proxy. This guide walks you through a production-grade Rails 7 install on Ubuntu 24.04 LTS, from a fresh SSH session to a working Puma + Sidekiq deployment behind Nginx with TLS.

Want a head start? Deploy a CloudCore Starter VPS pre-tuned for Ruby workloads and follow this guide end to end. Launch a Rails-ready VPS now and you will be serving your first request in under 30 minutes.

Table of Contents

  • What You Will Build
  • Why Self-Host Rails on a VPS?
  • Prerequisites
  • Step 1: Update System Packages and Install Build Dependencies
  • Step 2: Create a Deploy User
  • Step 3: Install rbenv and ruby-build
  • Step 4: Install Ruby 3.3.x
  • Step 5: Install Bundler and Rails
  • Step 6: Install and Configure PostgreSQL 16
  • Step 7: Install Redis for Sidekiq and Action Cable
  • Step 8: Create or Clone Your Rails App
  • Step 9: Configure Credentials and SECRET_KEY_BASE
  • Step 10: Install Gems, Migrate, and Precompile Assets
  • Step 11: Configure Puma for Production
  • Step 12: Create systemd Units for Puma and Sidekiq
  • Step 13: Configure Nginx as a Reverse Proxy
  • Step 14: Enable TLS with Let's Encrypt
  • Step 15: The Deploy Flow
  • Alternative: Deploying with Kamal 2
  • Troubleshooting
  • FAQ
  • Next Steps
  • What You Will Build

    By the end of this guide your VPS will be running a full Rails 7 production stack:

    • Ruby 3.3.x managed via rbenv (multiple Ruby versions coexist cleanly)
    • Rails 7.1+ with Propshaft or Sprockets assets and Import Maps / jsbundling
    • PostgreSQL 16 as the primary data store, with a dedicated app role
    • Redis 7 backing Sidekiq background jobs and Action Cable WebSockets
    • Puma as the app server with multiple workers and preload_app!
    • systemd units supervising Puma and Sidekiq (auto-restart, log journal, boot)
    • Nginx reverse proxy fronting Puma over a Unix socket with try_files $uri @puma
    • Let's Encrypt TLS with auto-renewal via Certbot
    Each piece is independent — you can swap Puma for Falcon, PostgreSQL for MySQL, or Nginx for Caddy without rewriting the rest of the stack.

    Why Self-Host Rails on a VPS?

    Managed platforms like Heroku, Render, and Fly.io abstract away the operating system, which is convenient until you need to tune it. Running Rails on your own VPS gives you:

    • Flat, predictable pricing — A VPS costs the same whether you serve 1,000 or 1,000,000 requests this month. No dyno-hours, no per-worker premiums, no surprise bandwidth bills.
    • Full control of the stack — Choose any Ruby version, any Postgres extension, any kernel tuning. Add pg_stat_statements, pgvector, timescaledb, or custom compiled gems with zero platform lock-in.
    • Colocated services — PostgreSQL, Redis, Sidekiq, and Puma share a localhost loopback. Intra-service latency drops to microseconds and egress bandwidth is free.
    • Resource headroom for background work — Sidekiq, Action Cable, Active Job, nightly ETL, and cron tasks all run on the same box without provisioning separate worker instances.
    • Compliance and data residency — You choose the data center. Keep EU customer data in Germany. Keep healthcare data on a HIPAA-eligible host. You control the data lifecycle end to end.
    • No cold starts — Your Puma workers stay warm forever. First-request latency stays under 100 ms even after a quiet hour.
    A single CloudCore Starter VPS (3 vCPU, 8 GB RAM) comfortably serves mid-traffic Rails apps — think SaaS products doing hundreds of thousands of requests per day with Sidekiq queues and WebSockets.

    Prerequisites

    Before you begin, make sure you have:

    • A VPS running Ubuntu 24.04 LTS with root or sudo access
    • SSH access to your server (PuTTY on Windows, or the built-in terminal on macOS/Linux)
    • A domain name pointed at your server's IP (for TLS in Step 14)
    • At least 2 GB of RAM for a single Puma worker + PostgreSQL + Redis (4-8 GB recommended for real workloads)
    • At least 20 GB of disk space for the OS, Ruby toolchain, Postgres data, and your app
    Recommended Plan: CloudCore Starter
    >
    For most Rails apps in production we recommend the CloudCore Starter plan:
    >
    - 3 vCPU cores
    - 8 GB RAM
    - 75 GB NVMe SSD
    - Unmetered bandwidth
    >
    This gives you room for 2-4 Puma workers, a healthy PostgreSQL buffer pool, Redis, and a Sidekiq process — plus headroom for traffic spikes. For heavier workloads, scale up to CloudCore Professional or beyond.

    Connect to your server via SSH to get started:

    bash
    ssh root@your-server-ip

    Step 1: Update System Packages and Install Build Dependencies

    Start by updating the package index and installing the libraries Ruby and common gems need to compile native extensions. Missing one of these is the single most common source of bundle install failures, so we install the full set up front.

    bash
    sudo apt update && sudo apt upgrade -y

    Install the build toolchain and library headers:

    bash
    sudo apt install -y \
      git curl wget ca-certificates gnupg lsb-release \
      build-essential autoconf bison \
      libssl-dev libyaml-dev libreadline-dev zlib1g-dev \
      libncurses-dev libffi-dev libgdbm-dev libdb-dev \
      libpq-dev libxml2-dev libxslt1-dev libvips imagemagick \
      pkg-config

    What each group of packages is for:

    • build-essential, autoconf, bison — gcc/make toolchain plus parser generators required to compile Ruby from source.
    • libssl-dev, libyaml-dev, libreadline-dev, zlib1g-dev, libffi-dev, libgdbm-dev — core Ruby dependencies for TLS, YAML parsing, interactive IRB, compression, FFI, and GDBM.
    • libpq-dev — PostgreSQL client headers needed by the pg gem. Skipping this is the number-one cause of bundle install failing on a Rails app.
    • libxml2-dev, libxslt1-dev — needed by nokogiri when it compiles against system libraries.
    • libvips, imagemagick — Active Storage image processing backends.
    If the kernel was updated, reboot before continuing:

    bash
    sudo reboot

    Step 2: Create a Deploy User

    Never run a Rails app as root. Create a dedicated unprivileged user that owns your code, gems, and Puma sockets.

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

    Copy your SSH key so you can log in as deploy:

    bash
    sudo rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy

    Switch to the new user for the rest of the guide:

    bash
    sudo su - deploy

    All subsequent commands assume you are the deploy user unless a step explicitly uses sudo.

    Step 3: Install rbenv and ruby-build

    rbenv is the de-facto Ruby version manager for production servers. It is lightweight, has no shell-function magic, and lets multiple Ruby versions coexist cleanly — which matters when you upgrade Rails across projects.

    Clone rbenv and the ruby-build plugin into the deploy user's home directory:

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

    Wire it into the shell:

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

    Verify rbenv is on PATH:

    bash
    rbenv --version

    Expected output:

    text
    rbenv 1.2.0-78-g89909fa

    Step 4: Install Ruby 3.3.x

    Rails 7.1+ officially supports Ruby 3.1, 3.2, and 3.3. We recommend Ruby 3.3 for any new install — it delivers YJIT improvements, GC compaction by default, and the fastest performance Ruby has ever shipped.

    List the Ruby versions ruby-build knows about:

    bash
    rbenv install --list | grep '^3\.3'

    Install the latest 3.3.x:

    bash
    rbenv install 3.3.6
    rbenv global 3.3.6

    The compile step takes 5-10 minutes on a 3 vCPU server. Expected final output:

    text
    Installed ruby-3.3.6 to /home/deploy/.rbenv/versions/3.3.6

    Verify:

    bash
    ruby -v

    Expected output:

    text
    ruby 3.3.6 (2024-11-05 revision 75015d4c1f) [x86_64-linux]

    Turn off documentation generation for gems

    Generating RDoc every time you install a gem wastes minutes and disk. Disable it globally:

    bash
    echo "gem: --no-document" >> ~/.gemrc

    Step 5: Install Bundler and Rails

    With Ruby in place, install Bundler (the gem dependency manager) and Rails:

    bash
    gem install bundler
    gem install rails -v '~> 7.1'
    rbenv rehash

    Verify:

    bash
    bundler -v
    rails -v

    Expected output:

    text
    Bundler version 2.5.23
    Rails 7.1.5
    rbenv rehash regenerates the shim files in ~/.rbenv/shims so new gem executables (rails, bundle, sidekiq, etc.) become available on PATH. Run it any time you install a gem that ships binaries.

    Step 6: Install and Configure PostgreSQL 16

    PostgreSQL is Rails' default production database and the right choice for almost every new app. Install version 16 from the official PostgreSQL APT repository (Ubuntu ships an older version by default).

    Switch back to a user with sudo:

    bash
    exit   # back to your sudo user

    Add the PGDG repository:

    bash
    sudo install -d /usr/share/postgresql-common/pgdg
    sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
      --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc

    echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] \ https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \ | sudo tee /etc/apt/sources.list.d/pgdg.list

    sudo apt update sudo apt install -y postgresql-16 postgresql-contrib-16

    Check it is running:

    bash
    sudo systemctl status postgresql

    Create a database role for your app

    Create a PostgreSQL role that matches the deploy OS user. This lets Rails connect via the Unix socket with no password.

    bash
    sudo -u postgres createuser --createdb --pwprompt deploy

    Enter a password when prompted (store it somewhere secure — you will put it in Rails credentials). Create the production database:

    bash
    sudo -u postgres createdb --owner=deploy myapp_production

    Verify the connection

    bash
    sudo su - deploy
    psql -d myapp_production -c 'SELECT version();'

    Expected output:

    text
    PostgreSQL 16.4 (Ubuntu 16.4-1.pgdg24.04+1) on x86_64-pc-linux-gnu ...

    For a deeper walkthrough of Postgres tuning, replication, and backups, see our PostgreSQL 16 on Ubuntu guide.

    Step 7: Install Redis for Sidekiq and Action Cable

    Redis is how modern Rails apps run background jobs (Sidekiq), broadcast WebSocket messages (Action Cable), and cache fragments and low-level data (Rails.cache). One Redis instance serves all three roles on a single VPS.

    Install from the Ubuntu repository (24.04 ships Redis 7):

    bash
    sudo apt install -y redis-server

    Configure it to start on boot and bind to localhost only (the default):

    bash
    sudo systemctl enable --now redis-server

    Verify:

    bash
    redis-cli ping

    Expected output:

    text
    PONG

    Harden Redis for production

    Even on a firewalled box, enable memory limits and persistence. Edit /etc/redis/redis.conf:

    bash
    sudo sed -i 's/^# maxmemory .*/maxmemory 512mb/' /etc/redis/redis.conf
    sudo sed -i 's/^# maxmemory-policy .*/maxmemory-policy allkeys-lru/' /etc/redis/redis.conf
    sudo systemctl restart redis-server

    For a deeper Redis tuning guide (persistence modes, replication, SSL), see Redis on Ubuntu.

    Step 8: Create or Clone Your Rails App

    Switch back to the deploy user and put your app in /home/deploy/apps/myapp:

    bash
    sudo su - deploy
    mkdir -p ~/apps
    cd ~/apps

    Option A: Start a fresh Rails 7 app

    bash
    rails new myapp --database=postgresql --css=tailwind
    cd myapp

    The --database=postgresql flag wires up the pg gem and a production database config pointing at PostgreSQL. --css=tailwind adds the tailwindcss-rails gem (swap for --css=bootstrap or omit entirely).

    Option B: Clone an existing Rails app from Git

    bash
    git clone [email protected]:your-org/myapp.git
    cd myapp

    Make sure Gemfile specifies a Ruby version compatible with what you installed:

    ruby
    # Gemfile
    ruby "3.3.6"

    Configure config/database.yml for production

    The generator sets sensible defaults, but confirm the production block uses environment variables for credentials:

    yaml
    production:
      <<: *default
      database: myapp_production
      username: deploy
      password: <%= ENV["MYAPP_DATABASE_PASSWORD"] %>
      host: localhost

    Step 9: Configure Credentials and SECRET_KEY_BASE

    Rails 7 ships with encrypted credentials (config/credentials.yml.enc) that replace the old secrets.yml. The file is committed to Git; the master.key that decrypts it is not.

    Generate a production master key

    If you started a fresh app, Rails already created config/master.key and added it to .gitignore. For a cloned app, you need to place the production master key on the server. Never commit master.key to Git.

    Copy the key from your local machine over SSH:

    bash
    # from your laptop
    scp config/credentials/production.key deploy@your-server-ip:~/apps/myapp/config/credentials/

    Or set the RAILS_MASTER_KEY environment variable in the systemd unit (we do this in Step 12).

    Edit production credentials

    From the server:

    bash
    cd ~/apps/myapp
    EDITOR="nano" bin/rails credentials:edit --environment production

    Rails decrypts the file, opens it in nano, and re-encrypts on save. Add the database password, any API keys, and SECRET_KEY_BASE:

    yaml
    secret_key_base: <long-random-string-from-rails-secret>
    database_password: <the-password-you-set-in-step-6>
    aws:
      access_key_id: ...
      secret_access_key: ...

    Generate a strong SECRET_KEY_BASE:

    bash
    bin/rails secret

    Paste the output as the secret_key_base value. Rails uses this to sign cookies, CSRF tokens, and encrypted session data — treat it like a root password.

    Step 10: Install Gems, Migrate, and Precompile Assets

    Install gems without development/test groups:

    bash
    bundle config set --local deployment 'true'
    bundle config set --local without 'development test'
    bundle install

    The --deployment flag locks Bundler to the exact versions in Gemfile.lock and installs gems into vendor/bundle. This matches what Capistrano and Kamal expect.

    Create and migrate the database:

    bash
    RAILS_ENV=production bin/rails db:create db:migrate

    If you already created the database in Step 6, db:create will be a no-op.

    Precompile assets. Rails 7 defaults to Propshaft for asset serving and Import Maps for JavaScript — neither requires Node.js on the server. If your app uses jsbundling-rails with esbuild/Webpack or cssbundling-rails, install Node first:

    bash
    # only if your app uses jsbundling or cssbundling
    curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
    sudo apt install -y nodejs

    Then precompile:

    bash
    RAILS_ENV=production SECRET_KEY_BASE=dummy bin/rails assets:precompile

    We pass a dummy SECRET_KEY_BASE because assets:precompile boots Rails, which requires it. The real one is read from credentials at runtime.

    Smoke-test the app in production mode:

    bash
    RAILS_ENV=production bin/rails server -p 3000

    Hit http://your-server-ip:3000 from a browser or curl. You should see your app (or the default Rails welcome page). Press Ctrl+C and move on — we will replace rails server with Puma under systemd.

    Step 11: Configure Puma for Production

    Puma is Rails 7's default app server. For production you want multiple workers (forked processes) with preload_app! enabled so each worker shares memory via copy-on-write, dramatically reducing RAM usage.

    Edit config/puma.rb:

    ruby
    # 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", 3) preload_app!

    rails_env = ENV.fetch("RAILS_ENV", "production") environment rails_env

    app_dir = File.expand_path("..", __dir__) shared_dir = "#{app_dir}/tmp"

    Listen on a Unix socket so Nginx can proxy to it

    bind "unix://#{shared_dir}/sockets/puma.sock"

    Logging

    stdout_redirect "#{shared_dir}/logs/puma.stdout.log", "#{shared_dir}/logs/puma.stderr.log", true

    PID and state files

    pidfile "#{shared_dir}/pids/puma.pid" state_path "#{shared_dir}/pids/puma.state"

    Allow Puma to be restarted by rails restart command

    plugin :tmp_restart

    Reconnect DB and Redis in each forked worker

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

    Create the directories Puma writes into:

    bash
    mkdir -p ~/apps/myapp/tmp/{sockets,pids,logs}

    Tuning the worker and thread counts

    A good starting formula for a 3 vCPU / 8 GB VPS:

    • WEB_CONCURRENCY=3 — one worker per vCPU. Each worker is a separate OS process.
    • RAILS_MAX_THREADS=5 — 5 threads per worker. With MRI's GIL, threads are useful for I/O-bound work (DB queries, HTTP calls).
    • Total concurrency = workers × threads = 15 in-flight requests.
    • RAM — each worker uses ~250-400 MB after preload. 3 workers ≈ 1 GB.
    Tune RAILS_MAX_THREADS and DB_POOL together — your database pool must be at least as large as your thread count:

    yaml
    # config/database.yml
    default: &default
      adapter: postgresql
      pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

    Step 12: Create systemd Units for Puma and Sidekiq

    systemd is the cleanest way to supervise Rails processes on Ubuntu — it handles restarts, log aggregation via journalctl, and boot ordering.

    Puma unit

    Create /etc/systemd/system/puma.service (as sudo):

    bash
    sudo tee /etc/systemd/system/puma.service > /dev/null <<'EOF'
    [Unit]
    Description=Puma HTTP Server for myapp
    After=network.target postgresql.service redis-server.service
    Requires=postgresql.service redis-server.service

    [Service] Type=simple User=deploy Group=deploy WorkingDirectory=/home/deploy/apps/myapp

    Environment=RAILS_ENV=production Environment=RACK_ENV=production Environment=RAILS_LOG_TO_STDOUT=1 Environment=RAILS_SERVE_STATIC_FILES=1 Environment=WEB_CONCURRENCY=3 Environment=RAILS_MAX_THREADS=5 EnvironmentFile=-/home/deploy/apps/myapp/.env.production

    ExecStart=/home/deploy/.rbenv/shims/bundle exec puma -C config/puma.rb ExecReload=/bin/kill -USR1 $MAINPID

    Restart=always RestartSec=3 KillMode=mixed TimeoutStopSec=30

    StandardOutput=journal StandardError=journal SyslogIdentifier=puma

    [Install] WantedBy=multi-user.target EOF

    Create .env.production in the app directory for any non-credential env vars (like RAILS_MASTER_KEY if you prefer env over file):

    bash
    # /home/deploy/apps/myapp/.env.production
    RAILS_MASTER_KEY=your-master-key-here
    chmod 600 /home/deploy/apps/myapp/.env.production

    Sidekiq unit

    Sidekiq processes background jobs pulled from Redis. Create /etc/systemd/system/sidekiq.service:

    bash
    sudo tee /etc/systemd/system/sidekiq.service > /dev/null <<'EOF'
    [Unit]
    Description=Sidekiq Background Worker for myapp
    After=network.target postgresql.service redis-server.service puma.service
    Requires=redis-server.service

    [Service] Type=simple User=deploy Group=deploy WorkingDirectory=/home/deploy/apps/myapp

    Environment=RAILS_ENV=production Environment=RAILS_LOG_TO_STDOUT=1 EnvironmentFile=-/home/deploy/apps/myapp/.env.production

    ExecStart=/home/deploy/.rbenv/shims/bundle exec sidekiq -e production -C config/sidekiq.yml

    Restart=always RestartSec=3 KillMode=mixed TimeoutStopSec=60

    StandardOutput=journal StandardError=journal SyslogIdentifier=sidekiq

    [Install] WantedBy=multi-user.target EOF

    Enable and start both services

    bash
    sudo systemctl daemon-reload
    sudo systemctl enable --now puma.service sidekiq.service
    sudo systemctl status puma.service
    sudo systemctl status sidekiq.service

    Tail logs with:

    bash
    sudo journalctl -u puma -f
    sudo journalctl -u sidekiq -f

    Step 13: Configure Nginx as a Reverse Proxy

    Nginx fronts Puma, terminates TLS, serves precompiled assets directly (much faster than proxying them to Rails), and handles connection pooling.

    Install Nginx:

    bash
    sudo apt install -y nginx

    Create /etc/nginx/sites-available/myapp:

    bash
    sudo tee /etc/nginx/sites-available/myapp > /dev/null <<'EOF'
    upstream puma {
        server unix:///home/deploy/apps/myapp/tmp/sockets/puma.sock fail_timeout=0;
    }

    server { listen 80; server_name myapp.example.com;

    root /home/deploy/apps/myapp/public;

    client_max_body_size 50m; keepalive_timeout 10;

    # Serve precompiled assets directly with far-future expires location ~ ^/(assets|packs|images|fonts|favicon\.ico|robots\.txt|sitemap\.xml)/ { gzip_static on; expires max; add_header Cache-Control public; access_log off; }

    # Fall through to Puma for everything else try_files $uri/index.html $uri @puma;

    location @puma { 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; proxy_pass http://puma;

    # WebSockets (Action Cable) proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";

    # Long-lived requests (streaming, uploads) proxy_read_timeout 300s; proxy_send_timeout 300s; }

    error_page 500 502 503 504 /500.html; keepalive_timeout 10; } EOF

    Enable the site and reload Nginx:

    bash
    sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
    sudo rm -f /etc/nginx/sites-enabled/default
    sudo nginx -t
    sudo systemctl reload nginx

    The deploy user's home directory needs to be traversable by www-data:

    bash
    sudo chmod o+x /home/deploy
    sudo chmod o+x /home/deploy/apps
    sudo chmod o+x /home/deploy/apps/myapp

    Visit http://myapp.example.com in a browser — you should see your Rails app served through Nginx. For a deeper dive on Nginx tuning (HTTP/2, gzip, brotli, rate limiting), see Nginx on Ubuntu.

    Step 14: Enable TLS with Let's Encrypt

    Certbot automates Let's Encrypt certificate issuance and renewal.

    bash
    sudo apt install -y certbot python3-certbot-nginx
    sudo certbot --nginx -d myapp.example.com

    Certbot will:

  • Prove domain ownership via the HTTP-01 challenge.
  • Install the certificate into /etc/letsencrypt/live/myapp.example.com/.
  • Rewrite your Nginx site config to listen on 443 with TLS and redirect 80 → 443.
  • Install a systemd timer that auto-renews certificates 30 days before expiry.
  • Verify renewal works without actually renewing:

    bash
    sudo certbot renew --dry-run

    Your site is now available at https://myapp.example.com with an A+ TLS grade.

    Step 15: The Deploy Flow

    With systemd supervising Puma and Sidekiq, every deploy is a small, predictable sequence. For a Git-based workflow:

    bash
    # on the server as deploy user
    cd ~/apps/myapp

    git pull origin main bundle install --deployment --without development test RAILS_ENV=production bin/rails db:migrate RAILS_ENV=production bin/rails assets:precompile

    restart app server and background workers

    sudo systemctl restart puma.service sudo systemctl restart sidekiq.service

    Zero-downtime restarts

    Puma supports phased restart — workers restart one at a time so the site never goes fully offline:

    bash
    sudo systemctl reload puma.service

    The ExecReload=/bin/kill -USR1 $MAINPID line in the systemd unit triggers Puma's USR1 signal, which performs a phased restart. Use restart (SIGTERM) after schema migrations that change Active Record classes; use reload for code-only changes.

    Wrap the flow in a script

    Put the sequence into bin/deploy.sh:

    bash
    #!/usr/bin/env bash
    set -euo pipefail

    cd ~/apps/myapp

    git pull origin main bundle install --deployment --without development test RAILS_ENV=production bin/rails db:migrate RAILS_ENV=production bin/rails assets:precompile

    sudo systemctl reload puma.service sudo systemctl restart sidekiq.service

    echo "Deploy complete at $(date)"

    bash
    chmod +x ~/apps/myapp/bin/deploy.sh

    Grant deploy the right to reload/restart only these specific services without a password, via /etc/sudoers.d/deploy-services:

    text
    deploy ALL=NOPASSWD: /bin/systemctl reload puma.service, /bin/systemctl restart puma.service, /bin/systemctl restart sidekiq.service

    Alternative: Deploying with Kamal 2

    If you prefer a modern container-based workflow, Kamal 2 (the 37signals-built successor to Capistrano) deploys Rails apps as Docker containers over SSH. Kamal handles zero-downtime rollouts, health checks, and a built-in proxy (kamal-proxy) that replaces Nginx entirely.

    The trade-off: Kamal requires Docker on the server and an image registry. The upside: deploys are atomic, rollbacks are one command, and the same config/deploy.yml works for one server or a fleet.

    A typical Kamal workflow, once configured:

    bash
    bundle add kamal
    bin/kamal setup         # first-time install
    bin/kamal deploy        # subsequent deploys
    bin/kamal app logs -f   # tail logs
    bin/kamal rollback      # instant rollback

    Kamal is an excellent choice for teams deploying to multiple servers, or for anyone who wants to keep the host OS pristine and ship everything as containers. For a single-server Rails 7 app, the rbenv + systemd + Nginx stack in this guide is simpler, uses less RAM, and keeps you closer to the metal.

    Troubleshooting

    ProblemCauseSolution
    bundle install fails with An error occurred while installing pgMissing PostgreSQL client headerssudo apt install -y libpq-dev and rerun bundle install
    bundle install fails compiling nokogiri native extensionMissing libxml2/libxslt or build toolssudo apt install -y build-essential libxml2-dev libxslt1-dev pkg-config
    LoadError: cannot load such file -- bundler/setupBundler not installed in current rbenv Rubygem install bundler && rbenv rehash
    Puma service fails with address already in use on the socketStale socket from a prior runrm ~/apps/myapp/tmp/sockets/puma.sock && sudo systemctl restart puma
    Nginx returns 502 Bad GatewayPuma socket missing or permission deniedCheck ls -la ~/apps/myapp/tmp/sockets/, verify chmod o+x /home/deploy, check journalctl -u puma -n 100
    ActiveRecord::ConnectionNotEstablished: FATAL: Peer authentication failedPostgres pg_hba.conf using peer auth for TCPConnect via Unix socket (remove host from database.yml) or edit pg_hba.conf to use md5
    Sidekiq service keeps restartingRedis not reachable or wrong URLredis-cli ping to verify Redis, set REDIS_URL=redis://localhost:6379/0 in .env.production
    ActionView::Template::Error: The asset "application.css" is not present in the asset pipelineAssets not precompiled or wrong manifestRun RAILS_ENV=production bin/rails assets:precompile, check public/assets/.manifest.json exists
    Rails logs show ArgumentError: Missing secret_key_baseCredentials not decryptingVerify RAILS_MASTER_KEY env var or config/credentials/production.key exists and matches
    PG::ConnectionBad: could not connect to server at bootPuma started before Postgressystemd unit already has After=postgresql.service — also add Requires=postgresql.service
    Action Cable WebSocket fails with 404Nginx missing Upgrade / Connection headersVerify the @puma block in /etc/nginx/sites-available/myapp includes the WebSocket headers from Step 13
    Errno::ENOENT: No such file or directory @ rb_sysopen - tmp/pids/puma.pidPuma tmp directories missingmkdir -p ~/apps/myapp/tmp/{sockets,pids,logs}

    Viewing logs

    The single most useful debugging tool is journalctl:

    bash
    sudo journalctl -u puma -f           # tail Puma
    sudo journalctl -u sidekiq -f        # tail Sidekiq
    sudo journalctl -u nginx -n 100      # last 100 Nginx lines
    tail -f ~/apps/myapp/log/production.log

    FAQ

    Which Ruby version should I use for a new Rails 7 app?

    Use the latest Ruby 3.3.x (3.3.6 at the time of writing). Rails 7.1 and 7.2 officially support Ruby 3.1, 3.2, and 3.3, but 3.3 ships the fastest YJIT to date — in production workloads you will see 10-20% lower CPU time compared to 3.2 on the same code. Avoid 3.0 and earlier on any new install; they are out of security support. Upgrade existing apps to 3.3 during your next scheduled maintenance window. rbenv makes coexisting versions trivial, so you can test locally before flipping production.

    Do I need Node.js on the server?

    Rails 7 defaults eliminate Node from the server for most apps. Propshaft (the default asset pipeline) serves precompiled CSS and JavaScript with no compilation step. Import Maps (default for JavaScript) lets browsers load ES modules directly from the asset host with no bundler. If you stick with these defaults, you do not need Node on the server — compile assets in CI or on your laptop and ship them with your deploy.

    You do need Node on the server if your app uses jsbundling-rails (esbuild/Rollup/Webpack) or cssbundling-rails (Tailwind via PostCSS, Bootstrap, Sass), and you precompile on the server rather than in CI. In that case install Node 20 LTS as shown in Step 10.

    How many Puma workers and threads should I configure?

    The starting formula is workers = number of vCPUs and threads per worker = 5. On a 3 vCPU / 8 GB VPS, that means WEB_CONCURRENCY=3 and RAILS_MAX_THREADS=5, yielding 15 concurrent in-flight requests with roughly 1 GB of RAM consumed by Puma.

    Threads help with I/O-bound work (DB queries, API calls, file uploads) because MRI's GIL releases during I/O. Workers help with CPU-bound work because each is a separate process. If your app is mostly database queries and external API calls, lean on threads; if it does heavy in-Ruby computation (image transforms, PDF generation), lean on workers.

    Your database.yml pool size must be at least as large as your thread count, or Active Record will raise ActiveRecord::ConnectionTimeoutError under load.

    Should I use Capistrano, Kamal, or just a shell script?

    For a single server, a shell script like the one in Step 15 is the simplest and most reliable option. It is explicit, easy to debug, and has zero external dependencies.

    Use Capistrano when you need the traditional current/, releases/, and shared folder pattern with instant rollback — Capistrano's mature ecosystem still has more plugins than anything else.

    Use Kamal 2 when you want containerized deploys, atomic image-based rollouts, and multi-server support with a single YAML config. Kamal is increasingly the default for new Rails apps at 37signals and similar shops.

    All three are fine. Pick the one that matches your team's operational skill set.

    How do I add SSL for Action Cable WebSockets?

    Once Certbot has added the TLS block to your Nginx config (Step 14), Action Cable works over wss://myapp.example.com/cable with no additional config — the Upgrade and Connection headers in the @puma location block handle the WebSocket handshake. In config/cable.yml, make sure production uses Redis:

    yaml
    production:
      adapter: redis
      url: redis://localhost:6379/1
      channel_prefix: myapp_production

    And config/environments/production.rb allows connections from your host:

    ruby
    config.action_cable.allowed_request_origins = [
      "https://myapp.example.com"
    ]

    How do I back up the Postgres database?

    Add a nightly pg_dump cron entry for the deploy user:

    bash
    crontab -e
    text
    0 3   * pg_dump -Fc myapp_production > /home/deploy/backups/myapp_$(date +\%Y\%m\%d).dump && find /home/deploy/backups -mtime +14 -delete

    For production-grade backups, push the dump to S3-compatible storage (Backblaze B2, Cloudflare R2, Wasabi) with rclone or aws s3 cp. Test restores quarterly — a backup you have never restored is not a backup.

    Next Steps

    Now that Rails is running on your VPS, here are recommended next steps to harden and extend your setup:

    • Enable application monitoring — Install AppSignal, Scout APM, or self-host Sentry to track request latency, exceptions, and Sidekiq queue health. Errors in production are invisible without it.
    • Set up database backups to object storage — Use rclone or aws s3 to ship nightly pg_dump archives to Backblaze B2 or Cloudflare R2. Backups on the same VPS are not backups.
    • Add a CDN in front of Nginx — Put Cloudflare or Bunny.net in front of your domain to cache assets globally, absorb traffic spikes, and block malicious bots. Rails' public/assets hash-fingerprinted filenames are perfectly cacheable.
    • Harden SSH — Disable password authentication, change the default port, and install Fail2Ban. See our Linux hardening guide for a complete checklist.
    • Add staging — Clone this setup to a second VPS and deploy a staging branch. Never test migrations on production — staging costs one small VPS and saves one very bad evening.
    • Explore the Rails ecosystem — Browse rubyonrails.org for the official guides, rbenv.org for Ruby version management patterns, and rubygems.org for the gem index.

    Skip the Manual Install — Launch a Rails-Ready VPS
    >
    Our CloudCore Starter plan is pre-tuned for Ruby workloads — the right kernel parameters, NVMe storage, and unmetered bandwidth to run Rails in production from day one.
    >
    - 3 vCPU cores, 8 GB RAM, 75 GB NVMe SSD
    - Ubuntu 24.04 LTS ready in under 60 seconds
    - Unmetered bandwidth, no per-request fees
    - Deploy Rails, PostgreSQL, Redis, Sidekiq, and Nginx on one box
    - Scale up in-place when your app grows
    >
    Deploy Your Rails VPS Now — start shipping today.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket