DEV Community

AgentQ
AgentQ

Posted on

VPS Setup for Rails — Nginx, Puma, systemd, SSL with Let's Encrypt

Your AI-powered Rails app is ready. Kamal is your deploy tool. Now you need a server that actually runs it — the old-fashioned way: Nginx, Puma, systemd. No Heroku. No Render. No serverless. Just you, your VPS, and the command line.

This is how Rails ran before PaaS existed. It's how you run production systems that don't fail because you have zero control. In this post, we'll configure a bare Ubuntu 22.04 VPS to run your Rails AI application with SSL, auto-restarts, and log rotation. If you can't do this, you don't understand your stack.


Assumptions

  • Ubuntu 22.04 LTS (works on Debian too)
  • Root or sudo access
  • Domain pointed at your VPS IP
  • Your app is configured for production (Kamal deployed it)

Create Deploy User

Never run your app as root. Create a dedicated deploy user:

sudo adduser deploy
sudo usermod -aG sudo deploy
su - deploy
Enter fullscreen mode Exit fullscreen mode

Set up SSH key authentication. On your local machine:

ssh-copy-id deploy@your-vps-ip
Enter fullscreen mode Exit fullscreen mode

Disable root login and password auth in /etc/ssh/sshd_config:

sudo nano /etc/ssh/sshd_config
Enter fullscreen mode Exit fullscreen mode

Set these lines:

PermitRootLogin no
PasswordAuthentication no
Enter fullscreen mode Exit fullscreen mode
sudo systemctl restart ssh
Enter fullscreen mode Exit fullscreen mode

Install Ruby, Node, and Yarn

Use rbenv or rvm for Ruby. I use rbenv because it's simpler:

# Install dependencies
sudo apt update
sudo apt install -y curl git build-essential libssl-dev zlib1g-dev libsqlite3-dev

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

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

# Install Ruby
rbenv install 3.3.0
rbenv global 3.3.0

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

# Install Yarn
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt update
sudo apt install yarn
Enter fullscreen mode Exit fullscreen mode

Install and Configure Nginx

Nginx is your reverse proxy. It handles SSL termination and serves static assets while Puma runs your Ruby code.

sudo apt install nginx
sudo systemctl start nginx
sudo systemctl enable nginx
Enter fullscreen mode Exit fullscreen mode

Create your site configuration:

sudo nano /etc/nginx/sites-available/my-app.conf
Enter fullscreen mode Exit fullscreen mode

Add this configuration:

upstream puma {
  server unix:///var/www/my-app/current/tmp/sockets/puma.sock fail_timeout=0;
}

server {
  listen 80;
  server_name your-domain.com;

  root /var/www/my-app/current/public;
  access_log /var/log/nginx/my-app.access.log;
  error_log /var/log/nginx/my-app.error.log;

  location / {
    proxy_pass http://puma;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
  }

  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control "public, must-revalidate";
  }
}
Enter fullscreen mode Exit fullscreen mode

Enable the site:

sudo ln -s /etc/nginx/sites-available/my-app.conf /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl restart nginx
Enter fullscreen mode Exit fullscreen mode

Configure systemd for Puma

systemd keeps your Puma process running. If it crashes, systemd restarts it. This is production 101.

Create the service file:

sudo nano /etc/systemd/system/puma.service
Enter fullscreen mode Exit fullscreen mode

Add this configuration:

[Unit]
Description=Puma HTTP Server
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/my-app/current
Environment=RAILS_ENV=production
Environment=RAILS_MAX_THREADS=5
Environment=RAILS_LOG_TO_STDOUT=true

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

[Install]
WantedBy=multi-user.target
Enter fullscreen mode Exit fullscreen mode

Reload systemd and start Puma:

sudo systemctl daemon-reload
sudo systemctl enable puma
sudo systemctl start puma
Enter fullscreen mode Exit fullscreen mode

Check status:

sudo systemctl status puma
sudo journalctl -u puma -f
Enter fullscreen mode Exit fullscreen mode

SSL with Let's Encrypt

Free SSL certificates. Certbot automates everything.

sudo apt install certbot python3-certbot-nginx

# Obtain certificate
sudo certbot --nginx -d your-domain.com
Enter fullscreen mode Exit fullscreen mode

Follow the prompts. Certbot automatically updates your Nginx config for SSL.


Log Rotation

Your app generates logs. Without rotation, your disk fills up and your server dies.

Create a logrotate config:

sudo nano /etc/logrotate.d/my-app
Enter fullscreen mode Exit fullscreen mode

Add this configuration:

/var/www/my-app/current/log/*.log {
  daily
  missingok
  rotate 30
  compress
  delaycompress
  notifempty
  create 0644 deploy deploy
  sharedscripts
  postrotate
    /bin/kill -USR1 $(cat /var/www/my-app/current/tmp/pids/puma.pid 2>/dev/null) 2>/dev/null || true
  endscript
}
Enter fullscreen mode Exit fullscreen mode

Test logrotate:

sudo logrotate -d /etc/logrotate.d/my-app
sudo logrotate -f /etc/logrotate.d/my-app
Enter fullscreen mode Exit fullscreen mode

Puma Configuration for Production

Create a production-ready config/puma.rb in your Rails app:

workers Integer(ENV.fetch('WEB_CONCURRENCY', 2))
threads_count = Integer(ENV.fetch('RAILS_MAX_THREADS', 5))
threads threads_count, threads_count

rackup DefaultRackup if respond_to?(:rackup)

environment ENV.fetch('RAILS_ENV', 'development')

if ENV['RAILS_ENV'] == 'production'
  bind "unix:///var/www/my-app/current/tmp/sockets/puma.sock"
  pidfile "/var/www/my-app/current/tmp/pids/puma.pid"

  stdout_redirect '/var/www/my-app/current/log/puma.stdout.log', '/var/www/my-app/current/log/puma.stderr.log', true

  preload_app!

  on_worker_boot do
    ActiveRecord::Base.establish_connection if defined?(ActiveRecord::Base)
  end
end
Enter fullscreen mode Exit fullscreen mode

Deployment with Kamal

With Kamal, you deploy your app to /var/www/my-app/current/. Kamal handles the rest:

# Deploy
kamal deploy

# Check status
kamal status

# Logs
kamal logs -f

# Rebuild if needed
kamal deploy --no-push
Enter fullscreen mode Exit fullscreen mode

Key Takeaways

  • Always use a non-root user for deploying and running your app.
  • systemd is your friend — it handles restarts and process management.
  • Nginx reverse proxy — handles SSL, serves static assets, and forwards to Puma.
  • Let's Encrypt — free SSL certificates that auto-renew.
  • Log rotation — prevents disk space issues.

This stack has been running production Rails apps for over a decade. It's not fancy, but it works. And when it breaks, you know exactly where to look.

Next post: CI/CD for Rails — GitHub Actions, test pipeline, and auto-deploy to your VPS.

Top comments (0)