Technology Encyclopedia Home >How to Deploy a Ruby on Rails App on a VPS — Puma, Nginx, and PostgreSQL

How to Deploy a Ruby on Rails App on a VPS — Puma, Nginx, and PostgreSQL

I picked up Ruby on Rails for a project a few years back. Writing the app was genuinely enjoyable — Rails makes so many things easy. Deploying it for the first time, though, was more involved than I expected.

The Ruby version management situation is the first thing that catches people off guard. Rails apps tend to be specific about Ruby versions, and the system Ruby on Ubuntu is usually too old. rbenv sorts this out cleanly once you know about it.

After that: Puma as the application server, PostgreSQL (Rails really wants PostgreSQL in production, not SQLite), Nginx in front, asset precompilation before the app starts. This guide walks through all of it in the right order.

I run this on Tencent Cloud Lighthouse with Ubuntu 22.04. Rails has a higher baseline memory requirement than Go or Node.js, so the 2 vCPU / 4 GB RAM plan is the right starting point. Ruby version management is where Rails deployments often get complicated — rbenv works well on Lighthouse's clean Ubuntu images since there are no pre-installed Ruby version conflicts to work around. The snapshot feature is particularly useful before db:migrate operations on production data, and the OrcaTerm browser terminal means I can check bundle exec rails console or review logs without needing my development machine.


Table of Contents

  1. The Rails Production Stack
  2. Prerequisites
  3. Part 1 — Server Setup
  4. Part 2 — Install Ruby with rbenv
  5. Part 3 — Install PostgreSQL
  6. Part 4 — Install Node.js and Yarn (for Asset Pipeline)
  7. Part 5 — Deploy the Rails Application
  8. Part 6 — Configure Puma
  9. Part 7 — Create a systemd Service for Puma
  10. Part 8 — Configure Nginx
  11. Part 9 — Enable HTTPS
  12. The Gotcha: Assets Not Loading in Production
  13. Deployment Workflow

Key Takeaways

  • Use rbenv to manage Ruby versions — system Ruby is usually too old for modern Rails
  • Run RAILS_ENV=production bundle exec rails assets:precompile before every deploy
  • Puma is the default Rails server — configure it with config/puma.rb
  • Set config.force_ssl = true in config/environments/production.rb
  • Backup database before every rails db:migrate on production

The Rails Production Stack {#stack}

Internet → Nginx → Puma (Unix socket) → Rails app
                ↓
          PostgreSQL database
Component Role
Nginx Serves static assets, terminates SSL, proxies app requests
Puma Multi-threaded Ruby application server
Rails Your application
PostgreSQL Database
rbenv Ruby version manager

Prerequisites {#prerequisites}

Requirement Notes
Cloud server Tencent Cloud Lighthouse Ubuntu 22.04
2 GB+ RAM Ruby has a higher memory footprint than Node.js or Go
A Rails app on GitHub We'll deploy from a git repository

Part 1 — Server Setup {#part-1}

ssh ubuntu@YOUR_SERVER_IP
sudo apt update && sudo apt upgrade -y

# Dependencies for Ruby and Rails
sudo apt install -y \
  git curl libssl-dev libreadline-dev zlib1g-dev \
  build-essential libpq-dev nodejs npm nginx \
  libffi-dev libyaml-dev

sudo ufw allow ssh
sudo ufw allow 'Nginx Full'
sudo ufw enable

Part 2 — Install Ruby with rbenv {#part-2}

Using rbenv (Ruby version manager) lets you install specific Ruby versions without root privileges and switch between them if needed.

# Install rbenv
curl -fsSL https://github.com/rbenv/rbenv-installer/raw/HEAD/bin/rbenv-installer | bash

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

# Verify
rbenv --version

Install Ruby (check your app's .ruby-version for the required version):

rbenv install 3.3.0
rbenv global 3.3.0

ruby --version   # ruby 3.3.0

# Install Bundler
gem install bundler

Part 3 — Install PostgreSQL {#part-3}

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

sudo -u postgres psql
CREATE DATABASE rails_production;
CREATE USER rails_user WITH PASSWORD 'choose_strong_password';
GRANT ALL PRIVILEGES ON DATABASE rails_production TO rails_user;
ALTER DATABASE rails_production OWNER TO rails_user;
\q

Part 4 — Install Node.js and Yarn (for Asset Pipeline) {#part-4}

Rails uses Node.js for JavaScript compilation (especially with Webpacker or importmap in newer versions):

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

npm install -g yarn

node --version
yarn --version

Part 5 — Deploy the Rails Application {#part-5}

cd ~
git clone https://github.com/your-username/your-rails-app.git myapp
cd myapp

# Install dependencies
bundle install --without development test

# Create credentials/environment config
EDITOR="nano" rails credentials:edit
# Or use environment variables (see below)

Create a .env file or use Rails credentials for sensitive values:

# Option: environment variables (simpler for VPS deployment)
cat > ~/.env << 'EOF'
RAILS_ENV=production
SECRET_KEY_BASE=$(rails secret)
DATABASE_URL=postgresql://rails_user:your_password@localhost/rails_production
EOF

Add to your shell profile:

echo 'set -a; source ~/.env; set +a' >> ~/.bashrc
source ~/.bashrc

Run setup tasks:

RAILS_ENV=production bundle exec rails db:migrate
RAILS_ENV=production bundle exec rails assets:precompile

Part 6 — Configure Puma {#part-6}

Puma is Rails' default application server. Configure it for production in config/puma.rb:

# 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

# Use a Unix socket for Nginx
bind "unix:///run/puma_myapp.sock"

# Workers (set to number of CPUs for MRI Ruby)
workers ENV.fetch("WEB_CONCURRENCY") { 2 }

# Use preload_app for faster worker restarts
preload_app!

# PID file
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }

Test Puma starts:

RAILS_ENV=production bundle exec puma -C config/puma.rb
# Ctrl+C to stop

Part 7 — Create a systemd Service for Puma {#part-7}

sudo nano /etc/systemd/system/puma_myapp.service
[Unit]
Description=Puma HTTP Server for myapp
After=network.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/myapp
EnvironmentFile=/home/ubuntu/.env
ExecStart=/home/ubuntu/.rbenv/shims/bundle exec puma -C config/puma.rb
ExecReload=/bin/kill -SIGUSR1 $MAINPID

Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl start puma_myapp
sudo systemctl enable puma_myapp
sudo systemctl status puma_myapp

# Verify the socket was created
ls /run/puma_myapp.sock

Part 8 — Configure Nginx {#part-8}

sudo nano /etc/nginx/sites-available/myapp
upstream puma_myapp {
    server unix:///run/puma_myapp.sock fail_timeout=0;
}

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    root /home/ubuntu/myapp/public;

    access_log /var/log/nginx/myapp_access.log;
    error_log  /var/log/nginx/myapp_error.log;

    # Serve static assets directly (bypasses Rails)
    location ^~ /assets/ {
        gzip_static on;
        expires max;
        add_header Cache-Control public;
    }

    # All other requests → Puma
    location / {
        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;

        if (-f $request_filename) { break; }
        if (-f $request_filename/index.html) { rewrite (.*) $1/index.html break; }
        if (-f $request_filename.html) { rewrite (.*) $1.html break; }

        if (!-f $request_filename) {
            proxy_pass http://puma_myapp;
            break;
        }
    }

    client_max_body_size 10m;
    keepalive_timeout 10;
}
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Part 9 — Enable HTTPS {#part-9}

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Add to Rails config in config/environments/production.rb:

config.force_ssl = true

Restart Puma:

sudo systemctl restart puma_myapp

The Gotcha: Assets Not Loading in Production {#gotcha}

Rails assets (CSS, JS) work fine in development but return 404 in production. This usually means:

  1. assets:precompile hasn't been run, or
  2. Nginx isn't configured to serve the public/assets/ directory correctly

Check:

# Verify assets are compiled
ls ~/myapp/public/assets/
# Should contain compiled CSS and JS files

# If empty, run:
RAILS_ENV=production bundle exec rails assets:precompile

Also check that Nginx's root directive points to myapp/public/ (not myapp/). Rails expects Nginx to serve from the public/ directory directly.


Deployment Workflow {#deployment}

After the initial setup, deploying updates looks like:

cd ~/myapp

# Pull latest code
git pull origin main

# Install any new gems
bundle install --without development test

# Run migrations
RAILS_ENV=production bundle exec rails db:migrate

# Recompile assets if changed
RAILS_ENV=production bundle exec rails assets:precompile

# Restart Puma gracefully (no downtime)
sudo systemctl reload puma_myapp
# Or full restart: sudo systemctl restart puma_myapp

Troubleshooting {#troubleshooting}

Issue Likely Cause Fix
Connection refused Service not running or wrong port Check systemctl status SERVICE and verify firewall rules
Permission denied Wrong file ownership or permissions Check file ownership with ls -la and use chown/chmod to fix
502 Bad Gateway Backend service not running Restart the backend service; check logs with journalctl -u SERVICE
SSL certificate error Certificate expired or domain mismatch Run sudo certbot renew and verify domain DNS points to server IP
Service not starting Config error or missing dependency Check logs with journalctl -u SERVICE -n 50 for specific error
Out of disk space Logs or data accumulation Run df -h to identify usage; clean logs or attach CBS storage
High memory usage Too many processes or memory leak Check with htop; consider upgrading instance plan if consistently high
Firewall blocking traffic Port not open in UFW or Lighthouse console Open port in Lighthouse console firewall AND sudo ufw allow PORT

Frequently Asked Questions {#faq}

Why use rbenv instead of the system Ruby?
Ubuntu's system Ruby is often outdated, and Rails apps are specific about Ruby versions. rbenv lets you install and switch between Ruby versions per-project without affecting system packages.

What is Puma and how does it differ from other Ruby app servers?
Puma is a concurrent Ruby/Rack web server optimized for multi-threaded handling of requests. It's the default application server for Rails since version 5. It handles concurrent requests more efficiently than single-threaded servers.

How do I precompile Rails assets for production?
Run RAILS_ENV=production bundle exec rails assets:precompile to compile CSS, JavaScript, and other assets into the public/assets/ directory. Configure Nginx to serve this directory directly.

What does rails db:migrate do and when should I run it?
It applies pending database schema changes (migrations) to the database. Run it after deploying new code that includes migration files. Always backup the database before running migrations in production.

How do I set Rails production environment variables?
Create /etc/environment entries, use a .env file with dotenv-rails gem, or use systemd's Environment= directive in the Puma service file. Never hardcode secrets in config/ files.

Deploy your Rails app today:
👉 Tencent Cloud Lighthouse — Ruby-ready Ubuntu VPS
👉 View current pricing and promotions
👉 Explore all active deals and offers