Deploy Ruby on Rails to Production: A Practical Guide

Khimananda Oli 8 min read Programming and Languages
Deploy Ruby on Rails to Production: A Practical Guide

By Khimananda Oli | Last reviewed: August 2026

You have built the application, but now you need to deploy Ruby on Rails to production without downtime or security gaps. Many teams struggle because they treat production as an afterthought rather than a distinct engineering discipline requiring specific web server configurations, process management, and hardening. This guide provides the exact architecture and configuration I use to ship resilient Rails applications in 2026, ensuring your stack is performant, observable, and secure from day one. For foundational server security before you begin, review my initial Ubuntu server setup guide to ensure your base OS is hardened against common attacks.

InternetHTTPS / TLSNginxReverse ProxyStatic AssetsTLS TerminationRate LimitingPuma ClusterApp Server (Systemd)Worker 1Worker 2Worker NSocket
Production architecture for deploying Ruby on Rails: Nginx handles external traffic and static files while Puma processes dynamic requests via Unix socket.

How do you configure Nginx and Puma to deploy Ruby on Rails to production?

The industry-standard stack for Rails in 2026 remains Nginx as the edge server and Puma as the application server. This separation of concerns is non-negotiable for production workloads. Nginx excels at buffering slow clients, serving static assets directly from disk, terminating TLS, and enforcing rate limits. Puma focuses exclusively on executing Ruby code. Connecting them via a Unix domain socket rather than TCP localhost reduces latency and prevents port conflicts.

Configuring Puma for production concurrency

Puma's configuration dictates your application's throughput and memory footprint. In config/puma.rb, set worker counts based on available CPU cores and RAM. A common mistake is over-provisioning workers, leading to OOM kills. For a 4GB VPS, start with 2 workers and 5 threads per worker.

# 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

worker_timeout 3600 if ENV["RAILS_ENV"] == "production"
workers ENV.fetch("WEB_CONCURRENCY", 2)
preload_app!

bind "unix:///var/www/myapp/shared/tmp/sockets/puma.sock"
pidfile ENV.fetch("PIDFILE", "tmp/pids/server.pid")
plugin :tmp_restart

The preload_app! directive enables copy-on-write memory sharing between workers, significantly reducing total RAM usage. Always bind to a socket path that persists across deployments, typically in a shared directory managed by Capistrano or Kamal.

Nginx reverse proxy configuration

Your Nginx config must handle upstream connections, asset caching headers, and security headers. Never expose Puma directly to the internet. The following block assumes you have obtained TLS certificates via Certbot, as detailed in my SSL setup guide.

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

server {
  listen 443 ssl http2;
  server_name example.com;
  
  root /var/www/myapp/current/public;
  
  # Security Headers
  add_header X-Frame-Options "SAMEORIGIN" always;
  add_header X-Content-Type-Options "nosniff" always;
  add_header Referrer-Policy "strict-origin-when-cross-origin" always;
  
  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control "public";
    access_log off;
  }
  
  try_files $uri @puma;
  
  location @puma {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_redirect off;
    proxy_pass http://puma_backend;
  }
}

Note the fail_timeout=0 in the upstream block. This tells Nginx not to mark the backend as down after a single failure, which is critical during zero-downtime restarts when the socket might briefly be unavailable.

How do you manage Rails processes with systemd in production?

Relying on tools like Foreman or raw nohup scripts in production is a liability. Systemd provides automatic restarts, logging integration, resource cgroups, and dependency ordering. When you deploy Ruby on Rails to production, systemd ensures your app survives crashes and boots correctly after server reboots.

Deploy ScriptSystemdPuma Processsystemctl reloadSIGUSR1 (phased)Old Workers DrainNew Workers BootZero Downtime: Socket stays active during phased restartNginx buffers requests until new workers are ready
Systemd orchestrates Puma phased restarts during Rails deployment, maintaining socket availability for zero-downtime releases.

Creating a production-ready systemd unit file

Create /etc/systemd/system/rails-app.service with explicit user isolation, environment loading, and restart policies. Never run Rails as root.

[Unit]
Description=Rails Application Server
After=network.target postgresql.service redis.service
Wants=postgresql.service redis.service

[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/var/www/myapp/current
EnvironmentFile=/var/www/myapp/shared/.env.production
ExecStart=/home/deploy/.rbenv/shims/bundle exec puma -C config/puma.rb
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=rails-app

# Security Hardening
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/var/www/myapp/shared/tmp /var/www/myapp/shared/log
PrivateTmp=true

[Install]
WantedBy=multi-user.target

The EnvironmentFile directive loads secrets outside version control. The ProtectSystem=strict combined with ReadWritePaths creates a sandbox where Puma can only write to tmp and log directories, limiting damage if compromised. After creating the file, run systemctl daemon-reload && systemctl enable --now rails-app.

What security hardening is required when deploying Rails to production?

Security cannot be bolted on after deployment. When you deploy Ruby on Rails to production, you must address secrets management, dependency vulnerabilities, and runtime isolation. Compliance frameworks like SOC 2 and ISO 27001 require evidence of these controls.

  • Secrets Management: Never commit credentials to Git. Use ENV variables injected via systemd EnvironmentFile, AWS Secrets Manager, or HashiCorp Vault. Rotate database passwords quarterly.
  • Dependency Scanning: Run bundle audit in CI to catch known CVEs. Integrate SCA tools like Trivy or Dependabot. For deeper pipeline security, see my article on shifting security left in CI/CD.
  • Runtime Isolation: Run Puma under a dedicated non-root user. Apply filesystem restrictions via systemd. Disable unused gems in production groups.
  • TLS Enforcement: Force HTTPS everywhere. Set config.force_ssl = true in production.rb to enable HSTS and secure cookies.
  • Database Access: Restrict PostgreSQL/MySQL to accept connections only from the app server IP or Unix socket. Never expose DB ports to 0.0.0.0.

How does containerized vs bare-metal Rails deployment compare in 2026?

The choice between containers and bare metal depends on team size, compliance needs, and operational maturity. Both are valid for deploying Ruby on Rails to production, but they optimize for different outcomes.

CriteriaBare Metal / VPS (Kamal/Capistrano)Containerized (Docker/Kubernetes)
Operational ComplexityLow — direct SSH, simple toolingHigh — requires registry, orchestration, networking
Cost EfficiencyHigh — no container overhead, smaller instancesModerate — K8s control plane adds baseline cost
Scaling SpeedMinutes — manual or script-based provisioningSeconds — HPA/VPA autoscaling
Compliance EvidenceManual — SSH logs, config auditsAutomated — image SBOMs, policy-as-code
Best ForSMBs, solo devs, Nepal-based startups on budgetScale-ups, multi-region, strict compliance

For most teams starting out, especially in cost-sensitive markets like Nepal, bare-metal deployment with Kamal offers the best balance of simplicity and professionalism. Containers become justified when you need auto-scaling, multi-region failover, or standardized platform engineering across multiple services.

Rails App Code + Tests PassBare Metal PathKamal / Capistrano → SSH → SystemdContainer PathDocker Build → Registry → K8s/ECSTrade-offs✓ Lower cost, simpler ops✓ Direct debugging access✗ Manual scaling✗ Drift risk without IaCTrade-offs✓ Auto-scaling, immutable deploys✓ Built-in SBOM & policy gates✗ Higher baseline cost✗ Steeper learning curve
Decision flowchart comparing bare-metal and containerized approaches when you deploy Ruby on Rails to production in 2026.

How do you monitor and maintain a production Rails deployment?

Deployment is not complete without observability. You cannot fix what you cannot measure. Configure structured logging in JSON format for machine parsing, and expose health endpoints for load balancer checks. Understanding the difference between metrics, logs, and traces is essential; my guide on observability signals compared breaks this down specifically for Rails applications.

Set up monitoring for the four golden signals: latency, traffic, errors, and saturation. Use Prometheus to scrape Puma's built-in stats endpoint (/metrics) and Grafana for visualization. Configure alerts for error rates exceeding 1% and p99 latency above 2 seconds. Automate log rotation to prevent disk exhaustion, and test backup restores monthly. Production readiness means assuming failure will happen and ensuring you can detect and recover from it within minutes, not hours.

Next steps for your Rails production deployment

When you deploy Ruby on Rails to production using this Nginx-Puma-systemd stack, you gain a foundation that scales from first customer to millions of requests. Start with bare metal if you are lean; graduate to containers when complexity demands it. Audit your secrets, enforce TLS, and wire up monitoring before announcing launch. If your team needs hands-on support architecting or hardening a Rails production environment, reach out through my contact page to discuss your specific infrastructure requirements.

Frequently Asked Questions

Use Ubuntu 24.04 LTS with Puma as the application server and Nginx as a reverse proxy. This combination offers excellent performance, stability, and community support for modern Rails applications in production environments today.

Set workers equal to CPU cores and threads between 5 and 16 based on available RAM. Enable preload_app for faster boot times and use systemd socket activation to handle zero-downtime restarts gracefully during deployments.

Both work well in 2026. Docker simplifies dependency management and scaling but adds orchestration complexity. Bare metal with Kamal or Capistrano reduces overhead and cost for single-server deployments under moderate traffic loads.

Match your Puma thread count exactly to the Active Record pool size in database.yml. Oversubscribing connections causes timeouts while undersubscribing wastes resources. Monitor pg_stat_activity regularly to validate sizing under real production load patterns.

Use Rails encrypted credentials or environment variables injected by your deployment tool. Never commit secrets to version control. Rotate keys quarterly and restrict access using IAM roles rather than long-lived static credentials wherever possible.

Yes, use Let's Encrypt with certbot auto-renewal via systemd timers.

Kamal uses containerized deployments over SSH without requiring Kubernetes, making it ideal for small teams. Capistrano deploys code directly to servers and remains preferable when you need granular hook control or legacy infrastructure compatibility in 2026.

Run rails assets:precompile during build and verify public/assets exists on the server. Check Nginx root directive points correctly and that fingerprinted filenames match manifest.json. Missing Sprockets or Propshaft configuration often causes silent failures in production.

Combine Sentry for error tracking, Prometheus with Grafana for metrics, and UptimeRobot for availability checks. Instrument key business transactions with custom counters to detect performance regressions before users report issues in production environments.

Use phased restarts via pumactl or systemd reload signals. Preload your application to fork workers from a warmed master process. Health check endpoints must return 200 only after initialization completes to prevent routing traffic to unready instances.

Enable AOF persistence with fsync every second and set maxmemory-policy to volatile-lru. Configure Sidekiq concurrency below your database connection limit. Monitor queue latency separately from processing time to distinguish infrastructure bottlenecks from application logic problems.

Absolutely. Rails 8 includes built-in Solid Queue and caching primitives reducing external dependencies significantly.

Limit Puma threads to reduce per-worker heap allocation. Enable jemalloc allocator and consider YJIT compilation for better throughput per megabyte. Profile with derailed_benchmarks to identify gem-level bloat consuming disproportionate memory in your specific application stack.

Schedule daily pg_dump compressed backups to object storage with 30-day retention. Test restores monthly in staging environments. Implement point-in-time recovery via WAL archiving for critical applications to minimize data loss during catastrophic failures or accidental deletions.

Enable request logging with rack-mini-profiler or scout_apm to identify N+1 queries and expensive serializations. Correlate slow endpoints with database query logs and cache hit ratios. Profile background jobs separately since they often compete for shared resources during peak hours.