How to Use Apache with Ruby on Rails

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

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

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

Performance Optimization Tips

Troubleshooting

502 Bad Gateway:

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

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

Passenger not starting the app:

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

References

Exit mobile version