How to Use Apache with Ruby on Rails

How to use Apache with Ruby on Rails

The first time I deployed a Rails app behind Apache, I assumed it would work like PHP — drop files in a folder and go. It doesn’t, and figuring that out the hard way taught me a lot about how Rails actually runs in production. In this guide, I’ll walk through the correct way to pair Apache with Rails using a reverse proxy setup, which is how virtually all production Rails deployments work today.

Understanding How Apache and Rails Work Together

Unlike PHP, Rails applications don’t run inside Apache. A Rails app is a standalone Ruby process (or set of processes) that runs independently, typically using an application server like Puma (the default since Rails 5), Unicorn, or Passenger.

Apache’s role is to sit in front of that Rails process as a reverse proxy, forwarding incoming HTTP requests to the Rails app server and returning its responses to the client. Apache also handles tasks Rails shouldn’t have to worry about directly: SSL termination, serving static assets efficiently, load balancing across multiple app instances, and request logging.

There are two common approaches:

  1. mod_proxy + Puma/Unicorn – Apache proxies requests to a Ruby app server running as a separate process. This is the modern, most common approach.
  2. Passenger (mod_passenger) – An Apache module that manages the Rails application lifecycle directly, so you don’t need to manually run and monitor a separate app server process.

This guide covers both.

Prerequisites

  • A Linux server with Apache 2.4+ installed
  • Ruby (2.7+, ideally 3.x) and Rails installed, along with a working Rails application
  • Root or sudo access
  • Basic familiarity with the command line and Rails’ directory structure
  • Bundler installed (gem install bundler)

Approach 1: Apache as a Reverse Proxy to Puma

Step 1: Install Required Apache Modules

sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod headers
sudo systemctl restart apache2

Step 2: Prepare Your Rails App

Inside your Rails app directory, make sure Puma is configured. Rails ships with a config/puma.rb file by default:

# 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

port ENV.fetch("PORT") { 3000 }

environment ENV.fetch("RAILS_ENV") { "production" }

pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }

Precompile assets and set up the production environment:

RAILS_ENV=production bin/rails assets:precompile
RAILS_ENV=production bin/rails db:migrate

Start Puma (in production, you’d typically run this under a process manager like systemd):

RAILS_ENV=production bundle exec puma -C config/puma.rb

Step 3: Create a systemd Service for Puma

Rather than running Puma manually, create a persistent service:

sudo nano /etc/systemd/system/puma.service
[Unit]
Description=Puma HTTP Server for MyRailsApp
After=network.target

[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/myrailsapp
Environment=RAILS_ENV=production
ExecStart=/usr/local/rvm/bin/rvm-exec 3.2.0 bundle exec puma -C config/puma.rb
Restart=always

[Install]
WantedBy=multi-user.target

Enable and start it:

sudo systemctl daemon-reload
sudo systemctl enable puma
sudo systemctl start puma

Step 4: Configure Apache as a Reverse Proxy

Create a virtual host:

<VirtualHost *:80>
    ServerName myrailsapp.com

    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/

    # Serve static assets directly from Apache for better performance
    Alias /assets /var/www/myrailsapp/public/assets
    <Directory /var/www/myrailsapp/public/assets>
        Require all granted
        Header set Cache-Control "public, max-age=31536000, immutable"
    </Directory>

    ErrorLog ${APACHE_LOG_DIR}/myrailsapp_error.log
    CustomLog ${APACHE_LOG_DIR}/myrailsapp_access.log combined
</VirtualHost>

Enable the site and restart Apache:

sudo a2ensite myrailsapp.conf
sudo apachectl configtest
sudo systemctl restart apache2

Approach 2: Using Passenger

Passenger integrates more tightly with Apache, managing the Rails process lifecycle for you, which simplifies deployment for many teams.

Step 1: Install Passenger

sudo apt install -y dirmngr gnupg apt-transport-https ca-certificates curl
curl https://oss-binaries.phusionpassenger.com/auto-software-signing-gpg-key.txt | sudo gpg --dearmor -o /usr/share/keyrings/phusion.gpg
sudo sh -c "echo 'deb [signed-by=/usr/share/keyrings/phusion.gpg] https://oss-binaries.phusionpassenger.com/apt/passenger jammy main' > /etc/apt/sources.list.d/passenger.list"
sudo apt update
sudo apt install -y libapache2-mod-passenger

Step 2: Enable the Module

sudo a2enmod passenger
sudo systemctl restart apache2

Verify installation:

sudo /usr/bin/passenger-config validate-install

Step 3: Configure the Virtual Host

<VirtualHost *:80>
    ServerName myrailsapp.com
    DocumentRoot /var/www/myrailsapp/public

    <Directory /var/www/myrailsapp/public>
        Allow from all
        Options -MultiViews
        Require all granted
    </Directory>

    PassengerRuby /usr/local/rvm/gems/ruby-3.2.0/wrappers/ruby
</VirtualHost>

With Passenger, there’s no need to manually manage a Puma process — Passenger starts and monitors Rails automatically based on the DocumentRoot pointing to public/.

Enabling SSL/HTTPS

Regardless of which approach you use, secure your Rails app with SSL using Let’s Encrypt:

sudo apt install certbot python3-certbot-apache
sudo certbot --apache -d myrailsapp.com

Certbot automatically updates your virtual host configuration to redirect HTTP to HTTPS and manages certificate renewal.

Real-World Use Cases

  • SaaS applications – Rails’ convention-over-configuration approach makes it popular for building subscription-based web apps, with Apache handling SSL and load balancing in front.
  • E-commerce platforms – Rails powers platforms like Shopify’s early architecture; Apache/Nginx reverse proxies are standard in front of such apps.
  • Internal business tools – Rails is a common choice for rapid internal tool development, often deployed behind Apache in corporate environments already standardized on Apache.
  • API backends – Rails in API-only mode paired with Apache as a proxy and rate-limiter for mobile or SPA frontends.

Common Mistakes to Avoid

  1. Forgetting to precompile assets – Running Rails in production without assets:precompile results in broken CSS/JS.
  2. Not setting RAILS_ENV=production – Running in development mode in production is a serious performance and security issue (verbose error pages leak information).
  3. Proxying static assets through Rails – Let Apache serve /public/assets directly rather than routing every asset request through the Ruby process.
  4. Missing ProxyPreserveHost – Without it, Rails may generate incorrect URLs (using the internal proxy address instead of the public domain).
  5. Running Puma as root – Always run application processes under a dedicated, non-privileged user.

Security Best Practices

  • Always run Rails in production mode with config.force_ssl = true in config/environments/production.rb.
  • Keep SECRET_KEY_BASE and other credentials out of version control; use Rails encrypted credentials or environment variables.
  • Set appropriate Content-Security-Policy and other security headers via mod_headers in Apache or Rails middleware.
  • Regularly run bundle audit to check for vulnerable gems.
  • Restrict direct access to port 3000 (or whatever Puma listens on) from outside the server — only Apache should be able to reach it, typically via binding Puma to 127.0.0.1.

Performance Optimization Tips

  • Serve static assets and uploaded files directly through Apache rather than proxying them through Rails.
  • Use a CDN for assets in high-traffic production apps.
  • Tune Puma’s worker/thread counts based on available CPU cores (WEB_CONCURRENCY and RAILS_MAX_THREADS).
  • Enable HTTP/2 in Apache for faster asset delivery on the client side.
  • Combine with the caching techniques from our Apache caching guide for cacheable pages.

Troubleshooting

502 Bad Gateway:

  • Puma isn’t running or isn’t listening on the expected port. Check with sudo systemctl status puma and curl http://127.0.0.1:3000.

Assets not loading (404s for CSS/JS):

  • Confirm assets:precompile ran successfully and the Alias path in your Apache config matches the actual public/assets directory.

“We’re sorry, but something went wrong” generic error page:

  • Check log/production.log in your Rails app directory for the actual stack trace.

Passenger not starting the app:

  • Run passenger-status to see running application instances, and check Apache’s error log for Passenger-specific startup errors.

Frequently Asked Questions

Should I use Puma or Passenger? Puma with a manual reverse proxy gives more control and is the more common modern pattern, especially in containerized/cloud deployments. Passenger is simpler to set up and manages the process lifecycle for you, which some teams prefer for traditional VPS deployments.

Can I run multiple Rails apps on one Apache server? Yes, using separate virtual hosts, each proxying to a different Puma instance on a different port (or Passenger handling each independently based on DocumentRoot).

Is Nginx better than Apache for Rails? Nginx is more commonly seen in Rails tutorials, but Apache works equally well as a reverse proxy. The choice often comes down to existing infrastructure and team familiarity rather than a hard technical requirement.

Do I need Apache at all, or can Puma serve requests directly? Puma can serve requests directly, but putting Apache in front provides SSL termination, static asset serving, load balancing, and additional security controls that are impractical to replicate in Puma alone.

Summary and Key Takeaways

  • Rails doesn’t run inside Apache; it runs as a separate process (via Puma, Unicorn, or Passenger), with Apache acting as a reverse proxy.
  • mod_proxy and mod_proxy_http are the key modules for proxying to Puma; Passenger offers a more integrated alternative.
  • Always run Rails in production mode, precompile assets, and serve static files directly through Apache.
  • Secure the deployment with SSL (Certbot/Let’s Encrypt), proper environment variables, and restricted access to the internal app server port.
  • Tune Puma’s thread/worker settings to match your server resources for the best performance.

References

Total
4
Shares

Leave a Reply

Previous Post
How to install and configure Python with Apache

How to install and configure Python with Apache

Next Post
How to enable server-side scripting in Apache

How to Enable Server-Side Scripting in Apache

Related Posts