Introduction
Running Nginx in front of a Rails application is standard practice in production โ Nginx handles static files, SSL termination, and load balancing, while Rails handles dynamic requests. This guide covers the essential Nginx configuration for Rails, including a common Turbolinks bug caused by missing proxy headers.
Basic Nginx Reverse Proxy for Rails
Start Rails on a specific port:
bundle exec rails s -p 3011 -b 0.0.0.0
Nginx configuration to proxy requests to Rails:
server {
listen 80;
server_name example.com;
root /data/myapp/public;
# Serve static assets directly (no Rails involvement)
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Serve Rails public directory
location /assets {
root /data/myapp/public;
expires 1y;
add_header Cache-Control "public, immutable";
}
# Proxy everything else to Rails
location / {
proxy_pass http://localhost:3011;
proxy_set_header Host $http_host; # critical โ see below
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;
}
}
The Critical Header: proxy_set_header Host
The most important line in the proxy configuration is:
proxy_set_header Host $http_host;
Without this, Nginx sends the backend’s address (localhost:3011) as the Host header instead of the actual domain. This causes several problems:
Turbolinks Redirect Bug
When Rails redirects (e.g., after form submission), it sets a Turbolinks-Location response header to tell Turbolinks what URL to show in the address bar. If the Host header is wrong, Rails generates an incorrect Turbolinks-Location:
# Without proxy_set_header Host:
Turbolinks-Location: http://localhost:3011/addresses/new โ wrong!
# With proxy_set_header Host $http_host:
Turbolinks-Location: http://localhost:8080/addresses/new โ correct
This causes a browser error:
Uncaught DOMException: Failed to execute 'replaceState' on 'History':
A history state object with URL 'http://localhost:3011/addresses/new'
cannot be created in a document with origin 'http://localhost:8080'
The browser refuses to update the URL because the redirect target has a different origin than the current page.
Why This Happens
Turbolinks makes requests via XMLHttpRequest, which follows redirects transparently. Turbolinks can’t detect a redirect happened without the Turbolinks-Location header. Rails sets this header automatically โ but uses the Host header to determine the correct URL. If Host is wrong, the generated URL is wrong.
From the Turbolinks documentation:
When you visit location /one and the server redirects you to location /two, you expect the browser’s address bar to display the redirected URL. Turbolinks makes requests using XMLHttpRequest, which transparently follows redirects. Send the
Turbolinks-Locationheader in response to a visit that was redirected, and Turbolinks will replace the browser’s topmost history entry with the value you provide.
Local Development Configuration
For local development with Nginx proxying to Rails:
# /etc/nginx/sites-available/myapp-dev
server {
listen 8080;
server_name localhost;
root /home/user/myapp/public;
# Static files
location /assets {
root /home/user/myapp/public;
try_files $uri =404;
}
# Uploaded files
location /uploads {
root /home/user/myapp/public;
}
# Proxy to Rails dev server
location / {
proxy_pass http://localhost:3011;
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_read_timeout 300;
proxy_connect_timeout 300;
}
}
Enable and reload:
sudo ln -s /etc/nginx/sites-available/myapp-dev /etc/nginx/sites-enabled/
sudo nginx -t # test config
sudo nginx -s reload
Production Configuration
A production-ready Nginx config for Rails with SSL:
# Redirect HTTP to HTTPS
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
# Main HTTPS server
server {
listen 443 ssl http2;
server_name example.com;
# SSL certificates (Let's Encrypt)
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
root /var/www/myapp/current/public;
# Security headers
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
# Gzip compression
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml;
# Static assets with long cache
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Puma socket (Unix socket is faster than TCP)
upstream rails_app {
server unix:///var/www/myapp/shared/tmp/sockets/puma.sock fail_timeout=0;
}
location / {
try_files $uri/index.html $uri @rails;
}
location @rails {
proxy_pass http://rails_app;
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 https;
proxy_redirect off;
proxy_read_timeout 300;
}
# Error pages
error_page 500 502 503 504 /500.html;
error_page 404 /404.html;
# Logs
access_log /var/log/nginx/myapp.access.log;
error_log /var/log/nginx/myapp.error.log;
}
Rails Configuration for Proxy
Tell Rails it’s behind a proxy:
# config/environments/production.rb
config.force_ssl = true
# Trust the proxy's X-Forwarded-For header
config.action_dispatch.trusted_proxies = [
"127.0.0.1",
"::1",
# Add your load balancer IPs here
]
# config/application.rb
# If behind a load balancer that sets X-Forwarded-Proto
config.middleware.insert_before ActionDispatch::SSL, Rack::Sendfile
Debugging Proxy Issues
# Test Nginx config syntax
sudo nginx -t
# Reload without downtime
sudo nginx -s reload
# Check what headers Rails receives
# Add to ApplicationController temporarily:
def debug_headers
render plain: request.headers.env.select { |k, _| k.start_with?('HTTP_') }.inspect
end
# Check Nginx error log
sudo tail -f /var/log/nginx/error.log
# Check Rails log for redirect issues
tail -f log/development.log | grep -E "Redirect|Location|Turbolinks"
Common Issues
| Problem | Cause | Fix |
|---|---|---|
| Turbolinks redirect error | Missing proxy_set_header Host |
Add proxy_set_header Host $http_host |
| 502 Bad Gateway | Rails not running or wrong port | Check Rails is running, verify port |
| Static files not served | Wrong root path | Check root directive points to public/ |
| SSL redirect loop | X-Forwarded-Proto not set |
Add proxy_set_header X-Forwarded-Proto $scheme |
| Slow uploads | No client_max_body_size |
Add client_max_body_size 50m |
Resources
- Nginx Reverse Proxy Documentation
- Turbolinks: Following Redirects
- Rails Behind a Proxy
- Capistrano + Puma + Nginx
Comments