Skip to main content

Rails + Puma + nginx — production setup

TL;DR

Complete guide to configuring Rails with Puma and nginx for production Linux deployments.

Key facts

Topology
Rails with Puma behind nginx

TL;DR

Rails with Puma and nginx is the standard production Ruby deployment stack. Puma runs your Rails application as a multi-process, multi-threaded application server, while nginx sits in front handling TLS termination, static file serving, and connection buffering via a Unix socket.

Architecture

Client → nginx (443/SSL, static assets) → Unix socket → Puma (workers × threads) → Rails

nginx handles slow clients and static assets. Puma handles Ruby code execution across forked worker processes. The Unix socket eliminates TCP overhead on the same host.

Puma configuration

Create or update 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", 2)
worker_timeout 30

preload_app!

bind "unix:///var/www/myapp/tmp/sockets/puma.sock"
environment ENV.fetch("RAILS_ENV", "production")
pidfile ENV.fetch("PIDFILE", "tmp/pids/server.pid")
state_path "tmp/pids/puma.state"

stdout_redirect "/var/log/puma/access.log", "/var/log/puma/error.log", true

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

Key settings explained:

  • workers — number of forked processes. Set to CPU core count for CPU-bound apps, or core count × 1.5 for I/O-bound apps
  • threads — threads per worker. 5 is a safe default. Higher values improve throughput for I/O-heavy apps but increase memory usage
  • preload_app! — loads the Rails application before forking workers. Reduces per-worker memory via copy-on-write, but requires on_worker_boot to re-establish database connections
  • Unix socket — lower overhead than TCP for same-host communication with nginx

Puma systemd service

Create /etc/systemd/system/puma.service:

[Unit]
Description=Puma Rails Server
After=network.target postgresql.service

[Service]
User=deploy
Group=deploy
WorkingDirectory=/var/www/myapp
ExecStart=/home/deploy/.rbenv/shims/bundle exec puma -C config/puma.rb
ExecReload=/bin/kill -USR1 $MAINPID
Restart=always
RestartSec=5
Environment=RAILS_ENV=production
EnvironmentFile=/var/www/myapp/.env
KillMode=mixed
TimeoutStopSec=30

[Install]
WantedBy=multi-user.target
sudo mkdir -p /var/log/puma
sudo chown deploy:deploy /var/log/puma
mkdir -p /var/www/myapp/tmp/sockets /var/www/myapp/tmp/pids
sudo systemctl daemon-reload
sudo systemctl start puma
sudo systemctl enable puma

nginx configuration

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

upstream rails_app {
    server unix:///var/www/myapp/tmp/sockets/puma.sock fail_timeout=0;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    root /var/www/myapp/public;
    client_max_body_size 10M;

    location / {
        try_files $uri @rails;
    }

    location @rails {
        proxy_pass http://rails_app;
        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;
        proxy_set_header X-Forwarded-Ssl on;
        proxy_read_timeout 60s;
        proxy_redirect off;
    }

    location /assets/ {
        expires 365d;
        add_header Cache-Control "public, immutable";
        gzip_static on;
    }

    location /packs/ {
        expires 365d;
        add_header Cache-Control "public, immutable";
        gzip_static on;
    }
}

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

The try_files $uri @rails directive serves files from public/ directly (static assets, favicons) and proxies everything else to Puma.

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx

SSL with Certbot

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

Environment variables

Store secrets in /var/www/myapp/.env (loaded by the systemd EnvironmentFile directive):

RAILS_ENV=production
SECRET_KEY_BASE=your-secret-key-here
DATABASE_URL=postgresql://deploy@localhost/myapp_production
RAILS_SERVE_STATIC_FILES=false

Set RAILS_SERVE_STATIC_FILES=false because nginx handles static files directly from the public/ directory.

With Reflex

Reflex monitors the entire Rails–Puma–nginx stack from a single agent. It tracks Puma worker health and thread utilisation, nginx upstream error rates, and system resources. During deployments, Reflex can orchestrate phased Puma restarts, run migrations, verify health endpoints, and automatically roll back if the application fails to respond. See How it works.