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:
- mod_proxy + Puma/Unicorn – Apache proxies requests to a Ruby app server running as a separate process. This is the modern, most common approach.
- 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
- Forgetting to precompile assets – Running Rails in production without
assets:precompileresults in broken CSS/JS. - Not setting RAILS_ENV=production – Running in development mode in production is a serious performance and security issue (verbose error pages leak information).
- Proxying static assets through Rails – Let Apache serve
/public/assetsdirectly rather than routing every asset request through the Ruby process. - Missing ProxyPreserveHost – Without it, Rails may generate incorrect URLs (using the internal proxy address instead of the public domain).
- Running Puma as root – Always run application processes under a dedicated, non-privileged user.
Security Best Practices
- Always run Rails in
productionmode withconfig.force_ssl = trueinconfig/environments/production.rb. - Keep
SECRET_KEY_BASEand other credentials out of version control; use Rails encrypted credentials or environment variables. - Set appropriate
Content-Security-Policyand other security headers viamod_headersin Apache or Rails middleware. - Regularly run
bundle auditto 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_CONCURRENCYandRAILS_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 pumaandcurl http://127.0.0.1:3000.
Assets not loading (404s for CSS/JS):
- Confirm
assets:precompileran successfully and the Alias path in your Apache config matches the actualpublic/assetsdirectory.
“We’re sorry, but something went wrong” generic error page:
- Check
log/production.login your Rails app directory for the actual stack trace.
Passenger not starting the app:
- Run
passenger-statusto 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_proxyandmod_proxy_httpare 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.