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:migrateoperations on production data, and the OrcaTerm browser terminal means I can checkbundle exec rails consoleor review logs without needing my development machine.
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:precompilebefore every deploy- Puma is the default Rails server — configure it with
config/puma.rb- Set
config.force_ssl = trueinconfig/environments/production.rb- Backup database before every
rails db:migrateon production
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 |
| 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 |
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
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
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
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
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
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
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
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
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
Rails assets (CSS, JS) work fine in development but return 404 in production. This usually means:
assets:precompile hasn't been run, orpublic/assets/ directory correctlyCheck:
# 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.
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
| 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 |
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.
/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