Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

The reliable production layout is client → Nginx → Puma → Rails. Nginx owns ports 80 and 443, serves files from Rails’ public directory, and forwards application requests to Puma on localhost or a Unix socket. Puma runs under systemd as a non-root service, while Rails credentials, the database, background jobs, TLS certificates, and uploads remain outside the Git checkout. This guide shows how to build that setup on an Ubuntu LTS VPS, release updates safely, and diagnose the failures that commonly appear after deployment.

What you are building

A VPS deployment has four operational layers:

  • Operating system: an Ubuntu LTS server, a dedicated deploy account, SSH hardening, firewall rules, time synchronization, and backups.
  • Rails release: the exact Git revision, the required Ruby version, a production bundle, credentials, database schema, and compiled assets.
  • Puma: Rails’ application server, bound only to localhost or a Unix socket and supervised by systemd.
  • Nginx: the public web server and reverse proxy that handles hostnames, static files, HTTP headers, request limits, and TLS.

Keep Puma off the public network interface when Nginx is the intended entry point. A local TCP listener such as 127.0.0.1:3000 is easy to inspect; a Unix socket avoids a TCP port and can be slightly simpler to isolate, but Nginx must be able to read the socket. Choose one endpoint and use the same value in both Puma and Nginx.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Before you deploy

Confirm the application contract

  • Read .ruby-version or the deployment policy and install that exact Ruby release.
  • Record the Rails version, the database adapter, JavaScript and asset tooling, and the command used to run background workers.
  • List every production environment variable and secret. Keep them in the host environment or a dedicated secret store, never in the repository.
  • Identify persistent data such as uploads, Active Storage files, compiled assets, and user-generated exports. These must live in a shared path or external storage rather than inside a disposable release directory.
  • Decide whether the application needs a queue worker, scheduled jobs, WebSockets, long requests, or large uploads; those decisions affect services and Nginx limits.

Choose a modest first VPS

Puma’s documentation recommends cluster mode when more than one CPU core is available and describes workers :auto as a starting point when concurrent-ruby is available. The default of five threads per worker is also a reasonable starting point, but memory and database capacity set the real limit. On a small VPS, start with one worker and a conservative thread count, observe resident memory and queue time, then increase concurrency deliberately.

Decision Start with Watch for
One CPU core Puma single mode CPU saturation and request latency
More than one CPU core Cluster mode; workers :auto can be a starting point Memory per worker, boot time, and database connections
Threads Five per worker, unless the workload requires less Database-pool exhaustion and memory growth
Listener Local TCP or a Unix socket Port conflicts, socket ownership, and Nginx reachability

Provision and secure the Ubuntu VPS

  1. Create the VPS with an Ubuntu LTS image and a hostname that matches the DNS name you will use. Apply the provider’s firewall and network policy before exposing the server.
  2. Create a non-root deploy account and install its SSH public key. Use the account for application files and systemd’s User=; reserve root for administration.
  3. Allow SSH only as required, open HTTP (80) and HTTPS (443), and deny other inbound ports unless the application explicitly needs them. A database should normally listen on a private interface or localhost, not on the public address.
  4. Install Nginx, Git, build tools, the database client libraries required by your adapter, and any system packages required by gems or asset compilation.
  5. Set the server timezone and enable automatic security updates according to your operating policy. Arrange database, upload, and configuration backups before the first production release.

Do not run Bundler, Rails migrations, or Puma as root. Root-owned files are a frequent cause of failed releases and can turn a compromised application into a host compromise.

#1 Best Overall
Elebase USB to USB C Adapter for iPhone 18 Pro Max,USBC Car Charger Adapter | 4Pack Type C Female to A Male Cable Converter for Apple Duo 17 16 15 14 Plus,iWatch Watch 12 11 10,iPad 9 8 Air,Airpods 5 4
  • Read Before You Buy — No Video Output: These adapters support charging and USB 2.0 data transfer, but cannot transmit video signals. Except for standard USB webcams (which use USB data only), they are not compatible with HDMI/DisplayPort cables, video-capable USB-C hubs, or docking stations with video output.
  • Convert USB-A Ports to USB-C: Designed to connect USB-C earphones, cables, flash drives, card readers, and other USB-C accessories to standard USB-A ports. Plug-and-play with no drivers or software required.
  • Aluminum Alloy Housing: Built with a sturdy aluminum alloy shell that aids in heat dissipation and protects against daily wear and scratches. Designed to maintain a stable and secure connection.
  • Compact & Travel-Friendly: The ultra-compact design allows the adapter to stay plugged into your device without blocking adjacent ports or adding bulk, reducing wear and tear on your original USB ports.
  • 12-Month Warranty: Backed by a 12-month manufacturer warranty for peace of mind. Designed to meet strict quality control standards for reliable everyday performance.

Install Ruby and build the Rails release

Match the project’s Ruby and Bundler versions

Install the Ruby version declared by the application, using the version-management method approved for the host. Verify it as the deploy user:

ruby -v
bundle -v
which ruby
which bundle

Systemd does not load the interactive shell’s PATH. Record the absolute path to the executable that will appear in the service’s ExecStart; do not assume that a command working in SSH will work from systemd.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Prepare a production checkout

cd /var/www/myapp
bundle config set deployment true
bundle config set without development test
bundle install

Use a release directory and a stable current symlink if you want atomic rollbacks:

mkdir -p /var/www/myapp/releases /var/www/myapp/shared
cd /var/www/myapp/releases
# fetch and check out the exact revision into a new timestamped directory

Place credentials, the production environment file, uploaded files, and any shared sockets or logs under /var/www/myapp/shared. Link those paths into each release. Set RAILS_ENV=production and verify every required credential before starting Puma.

Rank #2
Sale
Anker USB-C Hub, 5-in-1 USB Hub for Laptops, 4K HDMI Multiport Adapter | USB C Dongle with 90W Max Power Delivery, 3×5Gbps USB A Data Ports, for MacBook, Dell, and More (Charger Not Included)
  • 5-in-1 USB-C Hub: Experience comprehensive connectivity featuring a Power Delivery input, two USB-A 2.0 ports, a USB-A 3.0 port, and an HDMI port. (Note: The USB-C power delivery input port is only for connecting an external wall charger to power your laptop and cannot power peripheral devices.)
  • 90W Pass-Through Charging: Achieve optimal charging with 90W pass-through power to your laptop, supported by a total input of 100W, with the hub reserving 10W for operational efficiency. (Note: Wall charger not included.)
  • Quick Data Transfers: Accelerate your productivity with rapid data transfers using a high-speed 5Gbps USB 3.0 port and two 480Mbps USB 2.0 ports.
  • 4K HDMI Display: Enhance your visual experience with a hub capable of delivering 4K resolution at 30Hz in both mirror and extend modes. Please note that this hub is compatible with MacBook (macOS 12 and newer), Windows 10 and 11, ChromeOS, and laptops equipped with DP Alt Mode and Power Delivery. Note: This device is not compatible with Linux.
  • What You Get: Anker USB-C Hub (5-in-1, 4K HDMI), welcome guide, 18-month warranty, and our friendly customer service.

Prepare the database and assets

Run the migration command appropriate to the application, preferably after checking that the migration is backward-compatible with the currently running release:

RAILS_ENV=production bundle exec rails db:prepare
RAILS_ENV=production bundle exec rails assets:precompile

db:prepare may create a database on a new host; on an established production database, operators often run the reviewed migrations explicitly. Asset compilation is required for applications that serve fingerprinted assets locally; applications using an external asset pipeline may use a different release step. Confirm that the compiled files are readable by Nginx.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Configure Puma

Create config/puma.rb in the release. This example uses a Unix socket, five threads, and an environment-controlled worker count:

environment ENV.fetch('RAILS_ENV', 'production')
threads_count = Integer(ENV.fetch('RAILS_MAX_THREADS', 5))
threads threads_count, threads_count

if ENV['WEB_CONCURRENCY']
  workers Integer(ENV['WEB_CONCURRENCY'])
else
  workers :auto
end

bind 'unix:///var/www/myapp/shared/tmp/sockets/puma.sock'
pidfile '/var/www/myapp/shared/tmp/pids/puma.pid'
state_path '/var/www/myapp/shared/tmp/pids/puma.state'
stdout_redirect '/var/www/myapp/shared/log/puma.stdout.log', '/var/www/myapp/shared/log/puma.stderr.log', true
preload_app!
plugin :tmp_restart

Create the socket and PID directories with ownership that permits the deploy user to write and Nginx to connect:

Rank #3
Sale
Anker USB C Hub, 7in1 Multi-Port USB Adapter, 4K@60Hz USBC to HDMI Splitter | 85W Max PD, 2 USB 3.0 & 1 USBC Data Ports, SD/TF Card Reader, for Laptops/Mac and Other Type C Devices (Charger Not Included)
  • Sleek 7-in-1 USB-C Hub: Features an HDMI port, two USB-A 3.0 ports, and a USB-C data port, each providing 5Gbps transfer speeds. It also includes a USB-C PD input port for charging up to 100W and dual SD and TF card slots, all in a compact design.
  • Flawless 4K@60Hz Video with HDMI: Delivers exceptional clarity and smoothness with its 4K@60Hz HDMI port, making it ideal for high-definition presentations and entertainment. (Note: Only the HDMI port supports video projection; the USB-C port is for data transfer only.)
  • Double Up on Efficiency: The two USB-A 3.0 ports and a USB-C port support a fast 5Gbps data rate, significantly boosting your transfer speeds and improving productivity.
  • Fast and Reliable 85W Charging: Offers high-capacity, speedy charging for laptops up to 85W, so you spend less time tethered to an outlet and more time being productive.
  • What You Get: Anker USB-C Hub (7-in-1), welcome guide, 18-month warranty, and our friendly customer service.
sudo install -d -o deploy -g www-data /var/www/myapp/shared/tmp/sockets
sudo install -d -o deploy -g deploy /var/www/myapp/shared/tmp/pids
sudo install -d -o deploy -g deploy /var/www/myapp/shared/log

If you prefer localhost TCP, replace the bind line with bind 'tcp://127.0.0.1:3000' and point Nginx at port 3000. Do not bind to 0.0.0.0 unless you intentionally want Puma exposed independently of Nginx.

Size workers and threads from measurements

  • Each worker has its own Ruby memory footprint. A setting that fits on a development laptop may exhaust a small VPS.
  • Every Puma thread that can use the database needs a connection in the Rails pool. Set the pool high enough for the total web concurrency, but leave connections for migrations and background jobs.
  • Increasing workers can improve CPU parallelism while increasing boot time and memory. Increasing threads can help I/O-heavy requests while increasing contention and database demand.
  • Measure queue time, latency, CPU utilization, resident memory, database wait time, and restart duration before changing values.

Run Puma with systemd

Systemd keeps Puma in the foreground, monitors it, and can restart it when it exits. Puma 5 removed daemonization, so avoid old guides that pass --daemon. Create /etc/systemd/system/myapp-puma.service:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
[Unit]
Description=Myapp Puma application server
After=network.target

[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/var/www/myapp/current
Environment=RAILS_ENV=production
EnvironmentFile=-/etc/myapp/myapp.env
ExecStart=/absolute/path/to/bundle exec puma -C /var/www/myapp/current/config/puma.rb
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Replace the executable path with the absolute path found as the deploy user. If Bundler is installed inside a version manager, use that installation’s absolute path or a small, root-owned wrapper whose path is absolute. Keep the environment file readable only by the accounts that need it:

sudo install -d -m 750 /etc/myapp
sudo chmod 640 /etc/myapp/myapp.env

Load and start the unit:

sudo systemctl daemon-reload
sudo systemctl enable myapp-puma.service
sudo systemctl start myapp-puma.service
sudo systemctl status myapp-puma.service
sudo journalctl -u myapp-puma.service -e

After editing the unit, always run daemon-reload before restarting. A failed service usually means a wrong absolute path, an unreadable working directory, missing credentials, a Ruby mismatch, a socket-directory permission problem, or a database connection failure.

Rank #4
Sale
UGREEN USB to USB C Adapter Combo 4-Pack, 10Gbps USB C Converter Space Gray | Data transfer & Fast charging for MacBook, iPhone 17/16/15, iPad, Samsung Galaxy, Apple Watch
  • Dual Converters, Infinite Potential:Includes 2× USB C male to USB A female adapters and 2× USB A male to USB C female adapters. Perfect for a wide range of uses—tablets with Bluetooth keyboards, expand USB ports on macbook, and more. Two different converters for all your daily needs
  • Next-Level 10Gbps & 3A Charging: No more slow 480Mbps, this usb to usb c adapter has a transfer speed of up to 10Gbps, allowing you to do more transferring in less time. This usb adapter fits both USB A and USB C charger, supporting up to 3A fast charging
  • Upgraded Exquisite Craftsmanship: With an aluminum alloy housing and metal connector, the usbc to usb adapter is extremely durable and sturdy. Rigorously tested to withstand more than 10,000 times of plugging and unplugging, ensuring long-lasting performance
  • Broad Compatible: The usb c to usb adapter widely supports all USB C/ USB A devices like laptops, tablets, cellphones, car chargers, and phone chargers. Such as compatible with MacBook Pro/Air 2023/2022, Thunderbolt 4/3 Devices,Apple MagSafe Watch 9/8/7/SE/Ultra, iPad Pro 2022/2021, Samsung Galaxy S23/S20/S10, and iPhone 17/16/15 Pro. Plug and play
  • Please Note: To reach 10Gbps speed, keep the cable under 3.3 ft. For USB A Male to USB C adapters, try flipping the USB C connector. USB C Male to USB A adapters support bidirectional 10Gbps transfer within 3.3 ft

Configure Nginx as the reverse proxy

Create an Nginx site for the real hostname. The following pattern serves existing files from Rails’ public directory and sends everything else to Puma:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
upstream myapp_puma {
    server unix:/var/www/myapp/shared/tmp/sockets/puma.sock fail_timeout=0;
}

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    root /var/www/myapp/current/public;

    client_max_body_size 25m;

    location /assets/ {
        try_files $uri =404;
        access_log off;
        add_header Cache-Control 'public, max-age=31536000, immutable';
    }

    location / {
        try_files $uri $uri/ @myapp;
    }

    location @myapp {
        proxy_set_header Host $http_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_redirect off;
        proxy_read_timeout 60s;
        proxy_pass http://myapp_puma;
    }
}

Save it under /etc/nginx/sites-available/myapp, enable it, and test before reloading:

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

Check the proxy contract

  • server_name must match the hostname in DNS and in the browser request.
  • The upstream must point to the exact Puma socket or localhost port.
  • Nginx must be able to traverse every parent directory and read the socket. Group ownership and mode are as important as the socket path.
  • Preserve the original Host, client address, and scheme. Rails uses these values for URL generation, redirects, logging, and secure-cookie behavior.
  • Set body size and timeouts to the application’s needs. Raising them globally can let a single request consume excessive memory or worker time.
  • Use nginx -t before every reload; a syntax error should never take down the currently running configuration.

Test locally before changing DNS:

curl -I http://127.0.0.1
curl -H 'Host: example.com' http://127.0.0.1/health

A local 502 means Nginx cannot connect to Puma. A local 200 followed by a public failure usually points to DNS, firewall, TLS, or the wrong Nginx server block.

Add HTTPS and canonical redirects

Point the domain’s DNS records at the VPS, verify that port 80 reaches the intended Nginx server, and then obtain a certificate with the certificate tooling approved for your distribution and organization. Automated Let’s Encrypt coverage is common, but the exact package, command, renewal timer, and Nginx integration vary by operating system.

Best Value
Sale
Anker USB C Hub, 5-in-1 USBC to HDMI Splitter with 4K Display | 85W Max PD, 1 Powered USB-C 5Gbps & 2 Powered USB-A 3.0 5Gbps Data Ports, for MacBook Pro, MacBook Air, Dell and More (Black)
  • 5-in-1 Connectivity: Equipped with a 4K HDMI port, a 5 Gbps USB-C data port, two 5 Gbps USB-A ports, and a USB C 100W PD-IN port. Note: The USB C 100W PD-IN port supports only charging and does not support data transfer devices such as headphones or speakers.
  • Powerful Pass-Through Charging: Supports up to 85W pass-through charging so you can power up your laptop while you use the hub. Note: Pass-through charging requires a charger (not included). Note: To achieve full power for iPad, we recommend using a 45W wall charger.
  • Transfer Files in Seconds: Move files to and from your laptop at speeds of up to 5 Gbps via the USB-C and USB-A data ports. Note: The USB C 5Gbps Data port does not support video output.
  • HD Display: Connect to the HDMI port to stream or mirror content to an external monitor in resolutions of up to 4K@30Hz. Note: The USB-C ports do not support video output.
  • What You Get: Anker 332 USB-C Hub (5-in-1), welcome guide, our worry-free 18-month warranty, and friendly customer service.

After the certificate is installed, add a separate port-80 server block that returns a permanent redirect to HTTPS, and configure the TLS server on port 443. Preserve the same root, upstream, proxy headers, and upload limits in the TLS block. Test renewal before relying on it; an expired certificate is an outage even when the Rails process is healthy.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Release a new version without losing control

A safe release separates building from switching traffic. The sequence below works with a current symlink and a systemd-managed Puma:

  1. Fetch and verify the exact commit into a new directory under /var/www/myapp/releases. Do not modify the directory that is currently serving traffic.
  2. Install the production bundle using the locked dependency files. Link shared credentials, uploads, sockets, logs, and other persistent paths.
  3. Run tests or smoke checks that are practical on the host. Compile assets and run reviewed database migrations. Prefer additive, backward-compatible migrations when old and new code may overlap.
  4. Start a one-off Rails task or inspect the release to confirm that RAILS_ENV=production, credentials, database connectivity, and asset paths are correct.
  5. Switch the symlink atomically so /var/www/myapp/current points to the prepared release:
ln -sfn /var/www/myapp/releases/20261015-120000 /var/www/myapp/current
  1. Restart Puma so workers load the new code, then inspect its status and recent journal entries:
sudo systemctl restart myapp-puma.service
sudo systemctl status myapp-puma.service
sudo journalctl -u myapp-puma.service -n 100 --no-pager
  1. Exercise the health endpoint, an authenticated page, a static asset, a database-backed request, uploads, redirects, and any background-job enqueue path. Watch Nginx access and error logs while doing so.
  2. If the release is unhealthy, point current back to the previous known-good directory and restart Puma. Keep database rollback scripts and a backup plan; reverting application code cannot automatically undo a destructive schema change.

A normal systemd restart is controlled but can interrupt in-flight requests. If uninterrupted releases are a hard requirement, use a Puma phased-restart design or run two independently monitored application pools behind Nginx; validate that design with the application’s database and connection-pool behavior before relying on it.

Verify the complete production path

  • Process: systemctl status myapp-puma is active, and the journal shows successful boots rather than repeated crashes.
  • Socket or port: the endpoint in Puma’s configuration exists and is reachable by Nginx.
  • Nginx: nginx -t passes; access logs show the expected hostname and status codes; error logs contain no permission or upstream errors.
  • Rails: the health endpoint returns success, production credentials load, the database responds, and secure redirects use the correct scheme.
  • Assets: fingerprinted CSS, JavaScript, and images return 200 responses from the intended public path.
  • Operations: background workers, scheduled tasks, uploads, log rotation, backups, certificate renewal, and alerting are all tested rather than merely installed.

Troubleshoot by symptom

Symptom Likely cause Checks and fix
Systemd exits immediately Wrong executable path, Ruby mismatch, missing environment, or boot exception Run journalctl -u myapp-puma -e; execute the absolute command as deploy from the service’s working directory.
Nginx returns 502 Puma is stopped, the socket path differs, or permissions block Nginx Check Puma status, confirm the socket exists, inspect Nginx’s error log, and verify directory traversal and group access.
Static files return 404 Assets were not compiled, the root points at the wrong release, or files are unreadable Inspect /var/www/myapp/current/public, rerun the production asset task, and test the exact URL shown in the page source.
Redirect loop or wrong scheme Forwarded scheme is missing or Rails proxy trust is inconsistent Send X-Forwarded-Proto from Nginx, use the documented Rails proxy configuration, and ensure only the intended HTTPS redirect is active.
Uploads fail with 413 Nginx body limit is smaller than the application requirement Raise client_max_body_size deliberately in the relevant server block and align Rails and storage limits.
Requests time out Slow code, exhausted threads or database connections, or a proxy timeout that is too short Measure queue time, database waits, worker memory, and request duration before changing thread or timeout values.
New code is not running The symlink changed but Puma workers still have the old code Restart or perform the configured phased restart, then confirm the release path in logs and a diagnostic endpoint.

Operate and tune the VPS after launch

Keep Nginx and Puma logs separate so an upstream failure can be distinguished from an application exception. Rotate logs, monitor disk space, and alert on repeated systemd restarts, 5xx responses, certificate expiry, database saturation, and memory pressure. A VPS with no swap policy, no backups, and no recovery test is not a finished deployment.

Revisit Puma sizing after traffic changes. Compare CPU utilization and latency with total resident memory, database-pool usage, background-job bursts, and restart duration. More workers are not automatically faster: they can multiply memory use and database connections. Likewise, five threads per worker is a starting point, not a promise that every Rails workload benefits from five.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

When to use a different deployment path

Current Rails guidance also presents container-based deployment and Kamal alongside an Ubuntu LTS server. Those approaches can package the application and automate more of the release process. A hand-managed Nginx, systemd, and Puma stack remains a valid choice when you need direct control over the host, networking, files, and rollback procedure. The trade-off is that you own Ruby installation, service units, TLS renewal, log rotation, upgrades, and operational recovery. Commands, Ruby versions, certificate tooling, and firewall behavior vary by distribution and provider, so treat the examples here as an adaptable baseline rather than a universal copy-and-paste script.

Final deployment checklist

  • Ubuntu LTS is patched; SSH and firewall policy are in place.
  • The deploy account owns application files; Puma does not run as root.
  • The Ruby and Bundler versions match the application.
  • Production secrets are outside Git and readable only where needed.
  • The database is migrated safely and backed up.
  • Assets are compiled and readable from the active public directory.
  • Puma binds only to the selected local socket or port and is enabled in systemd.
  • The systemd unit uses an absolute ExecStart, has been daemon-reloaded, and restarts predictably.
  • Nginx preserves host, client, and scheme headers; nginx -t passes before reload.
  • HTTP and HTTPS behavior, certificate renewal, health checks, uploads, jobs, logs, backups, and rollback have all been tested.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.