Deploying Ruby on Rails Apps (Puma + Nginx)
Deploying a Ruby on Rails application to a production server typically involves a five-layer stack: rbenv for Ruby version management, Bundler for Gem dependency resolution, Puma as the application server registered as a systemd service, and Nginx as a reverse proxy that forwards requests to Puma via a Unix socket. Understanding the role of each tool and setting them up in order gives you a stable production environment.
Syntax
# -----------------------------------------------
# 1. Install rbenv and ruby-build
# -----------------------------------------------
# Install rbenv itself via git into your home directory
git clone https://github.com/rbenv/rbenv.git ~/.rbenv
# Add the rbenv bin directory to PATH
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
source ~/.bashrc
# Add the ruby-build plugin (required to build Ruby binaries)
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build
# List the Ruby versions available for installation
rbenv install --list
# -----------------------------------------------
# 2. Install Ruby and set the global version
# -----------------------------------------------
# Build and install Ruby 3.3.0 (this takes a few minutes)
rbenv install 3.3.0
# Set the Ruby version to use system-wide
rbenv global 3.3.0
# Verify the current Ruby version
ruby -v
# Regenerate rbenv shims (also required after installing new gems)
rbenv rehash
# -----------------------------------------------
# 3. Install Bundler and resolve app dependencies
# -----------------------------------------------
# Install Bundler (the Gem management tool for Rails apps)
gem install bundler
# Move to the application directory
cd /var/www/shinji-rails
# Install production Gems according to Gemfile.lock
# Deployment mode prevents changes to Gemfile.lock
bundle install --deployment --without development test
# -----------------------------------------------
# 4. Configure the Rails app for production
# -----------------------------------------------
# Run database migrations
RAILS_ENV=production bundle exec rails db:migrate
# Precompile assets (optimizes CSS and JS)
RAILS_ENV=production bundle exec rails assets:precompile
# Set the Rails master key and secret key as environment variables
# (Add these to /etc/environment or systemd Environment= directives)
# RAILS_MASTER_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# SECRET_KEY_BASE=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# -----------------------------------------------
# 5. Puma configuration file (config/puma.rb)
# -----------------------------------------------
# Specify the Unix socket path Puma will use
# bind 'unix:///var/run/shinji-rails/puma.sock'
# Set the number of workers and threads
# workers 2
# threads 4, 4
# Set the path for the PID file
# pidfile '/var/run/shinji-rails/puma.pid'
# -----------------------------------------------
# 6. Register Puma as a systemd service
# -----------------------------------------------
# Create the service file (see the Examples section below)
# sudo vim /etc/systemd/system/shinji-puma.service
# Reload the systemd daemon configuration
sudo systemctl daemon-reload
# Start the Puma service
sudo systemctl start shinji-puma
# Enable automatic startup on OS boot
sudo systemctl enable shinji-puma
# Check the service status
sudo systemctl status shinji-puma
# -----------------------------------------------
# 7. Configure Nginx as a reverse proxy
# -----------------------------------------------
# Create the Nginx configuration file (see the Examples section below)
# sudo vim /etc/nginx/sites-available/shinji-rails.conf
# Create a symbolic link in sites-enabled to activate the configuration
sudo ln -s /etc/nginx/sites-available/shinji-rails.conf \
/etc/nginx/sites-enabled/shinji-rails.conf
# Verify the configuration syntax
sudo nginx -t
# Reload Nginx to apply the configuration
sudo systemctl reload nginx
Syntax Reference
| Step | Description |
|---|---|
| Install rbenv | Clone it to ~/.rbenv via git, then append PATH and rbenv init to .bashrc. Also add the ruby-build plugin. |
| Install Ruby | rbenv install {version} builds the specified version. rbenv global {version} sets the system-wide default. |
| Set up Bundler | Install with gem install bundler, then run bundle install --deployment --without development test to install production Gems into the project. |
| DB migration | RAILS_ENV=production bundle exec rails db:migrate brings the database schema up to date. Run this on every deployment. |
| Asset precompilation | RAILS_ENV=production bundle exec rails assets:precompile optimizes CSS, JS, and images. Output is written to public/assets/. |
| Puma configuration | Specify the Unix socket path, number of workers, number of threads, and PID file path in config/puma.rb. |
| Register systemd service | Create /etc/systemd/system/{service_name}.service, then enable it in order: systemctl daemon-reload → systemctl start → systemctl enable. |
| Nginx reverse proxy configuration | Set proxy_pass to http://unix:/path/to/puma.sock to forward requests from Nginx to Puma's Unix socket. |
| Enable Nginx configuration | Create the configuration file in sites-available/ and create a symbolic link in sites-enabled/. Verify the syntax with nginx -t, then apply with systemctl reload nginx. |
| Check service status | systemctl status shinji-puma shows the running state of Puma. Use journalctl -u shinji-puma -f to follow real-time logs. |
Examples
/etc/systemd/system/shinji-puma.service
# ----------------------------------------------- # Configuration file for managing Puma as a systemd service # ----------------------------------------------- [Unit] # A description of the service Description=Puma HTTP Server for shinji-rails # Start this service after the network is available After=network.target [Service] # The user and group that will run the service User=shinji Group=www-data # The root directory of the Rails application WorkingDirectory=/var/www/shinji-rails # Set shell variables needed to start Puma via rbenv Environment=RAILS_ENV=production Environment=PATH=/home/shinji/.rbenv/shims:/home/shinji/.rbenv/bin:/usr/local/bin:/usr/bin:/bin Environment=RAILS_MASTER_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Start Puma via bundle exec # The settings in config/puma.rb will be loaded ExecStart=/home/shinji/.rbenv/shims/bundle exec puma -C /var/www/shinji-rails/config/puma.rb # On stop, send SIGTERM and wait for a graceful shutdown ExecStop=/bin/kill -s SIGTERM $MAINPID # Automatically restart the process if it crashes Restart=on-failure RestartSec=5 # Send stdout and stderr to journald StandardOutput=journal StandardError=journal # Create the directory for the Unix socket before starting RuntimeDirectory=shinji-rails RuntimeDirectoryMode=0755 [Install] # Start automatically in normal multi-user mode WantedBy=multi-user.target
/etc/nginx/sites-available/shinji-rails.conf
# -----------------------------------------------
# Configuration for Nginx to forward requests to Puma's Unix socket
# -----------------------------------------------
# Define the location of the Puma socket in an upstream block
upstream shinji_puma {
# Communicate via Unix socket (lower latency than TCP)
server unix:///var/run/shinji-rails/puma.sock fail_timeout=0;
}
server {
# Accept HTTP requests on port 80
listen 80;
# The domain name this virtual host handles
server_name shinji-rails.example.com;
# Set the Rails public/ directory as the document root for static files
root /var/www/shinji-rails/public;
# Specify the paths for the access log and error log
access_log /var/log/nginx/shinji-rails_access.log;
error_log /var/log/nginx/shinji-rails_error.log;
# Set the maximum upload size from clients
client_max_body_size 10M;
location / {
# Serve static files from public/ directly if they exist
# Otherwise, forward to Puma (@shinji_puma)
try_files $uri/index.html $uri @shinji_puma;
}
# Forward to Puma when no static file is found
location @shinji_puma {
# Forward to the shinji_puma socket defined in upstream
proxy_pass http://shinji_puma;
# Pass the request headers correctly to the app
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
# Do not rewrite redirects on the Nginx side
proxy_redirect off;
# Disable response buffering for WebSocket support
proxy_buffering off;
}
# Block direct access to sensitive files such as Puma's PID and socket
location ~ ^/(\.git|\.env|Gemfile) {
deny all;
}
# Apply long-term caching to asset files
location ~ ^/assets/ {
gzip_static on;
expires max;
add_header Cache-Control public;
}
}
Run the following command:
$ sudo systemctl status shinji-puma
● shinji-puma.service - Puma HTTP Server for shinji-rails
Loaded: loaded (/etc/systemd/system/shinji-puma.service; enabled; vendor preset: enabled)
Active: active (running) since Wed 2026-03-25 10:00:00 JST; 2min ago
Main PID: 12345 (ruby)
Tasks: 9 (limit: 4915)
Memory: 128.3M
CPU: 3.241s
CGroup: /system.slice/shinji-puma.service
└─12345 puma 6.4.0 (unix:///var/run/shinji-rails/puma.sock) [shinji-rails]
Mar 25 10:00:00 server shinji-puma[12345]: Puma starting in cluster mode...
Mar 25 10:00:00 server shinji-puma[12345]: * Puma version: 6.4.0
Mar 25 10:00:00 server shinji-puma[12345]: * Workers: 2
Mar 25 10:00:00 server shinji-puma[12345]: * Threads: 4
Mar 25 10:00:00 server shinji-puma[12345]: * Listening on unix:///var/run/shinji-rails/puma.sock
Mar 25 10:00:00 server shinji-puma[12345]: Use Ctrl-C to stop
$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
$ sudo systemctl reload nginx
$ curl -I http://shinji-rails.example.com/
HTTP/1.1 200 OK
Server: nginx/1.24.0
Content-Type: text/html; charset=utf-8
X-Request-Id: a1b2c3d4-e5f6-7890-abcd-ef1234567890
X-Runtime: 0.042
Overview
The production deployment architecture for a Ruby on Rails application is built on a five-layer stack: rbenv, Bundler, Puma, systemd, and Nginx. rbenv lets you install multiple Ruby versions side by side on a server and switch between them per project. Bundler locks Gem versions against Gemfile.lock, eliminating differences between the development and production environments. Puma is a multi-threaded application server that processes Rails requests, and by listening on a Unix socket it achieves lower latency than TCP. Registering it as a systemd service enables automatic startup after an OS reboot and automatic recovery from crashes (see the systemd unit page for details). Nginx acts as a reverse proxy, serving static files directly while forwarding only dynamic requests to the Puma socket. This setup lets Nginx handle static file delivery at high speed, concentrating Rails' processing resources exclusively on dynamic requests for an efficient production environment.
If you find any errors or copyright issues, please contact us.