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_bootto 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.