How to Properly Configure Nginx Reverse Proxy for Heroku App in a Subdirectory


3 views

When setting up a reverse proxy for a Heroku app under a subdirectory (e.g., /blog), many developers encounter 404 errors for CSS/JS assets. This occurs because the proxied application generates absolute paths that don't account for the subdirectory prefix.

Here's the correct way to configure your nginx server block:

server {
    listen 80;
    server_name mysite.com;

    location /blog/ {
        proxy_pass http://lovemaple.heroku.com/;
        proxy_redirect off;
        proxy_set_header Host $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;
        
        # Handle rewriting of HTML content
        sub_filter_once off;
        sub_filter 'href="/' 'href="/blog/';
        sub_filter 'src="/' 'src="/blog/';
        sub_filter 'url(/' 'url(/blog/';
    }
}

The critical components that solve this issue:

  • The trailing slash in both location /blog/ and proxy_pass URLs
  • sub_filter directives to rewrite absolute paths in HTML/CSS
  • Proper header forwarding to maintain request context

For more complex applications, you might need:

# For WebSocket proxying
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

# For large file uploads
client_max_body_size 100M;
proxy_read_timeout 300;

Always validate your setup:

nginx -t          # Test configuration syntax
nginx -s reload   # Apply changes without downtime

Check your browser's network tab to verify all assets load correctly from the /blog/ subdirectory.


When setting up a reverse proxy for a Heroku app under a subdirectory like /blog, developers often encounter resource loading issues where CSS/JS files return 404 errors. This occurs because the application generates absolute paths without accounting for the proxy prefix.

The original configuration:

location /blog {
    rewrite /blog/(.*) /$1 break;
    proxy_pass http://lovemaple.heroku.com;
    proxy_redirect off;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

While this routes requests to Heroku correctly, it doesn't handle these critical scenarios:

  • Application generates root-relative paths (/style.css)
  • Redirects from Heroku don't preserve the /blog prefix
  • Static assets lose their base path context

Here's the improved configuration that handles all edge cases:

location /blog/ {
    proxy_pass http://lovemaple.heroku.com/;
    proxy_http_version 1.1;
    
    # Essential headers for path preservation
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Port $server_port;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Prefix /blog;
    
    # Handle redirects properly
    proxy_redirect http://lovemaple.heroku.com/ /blog/;
    proxy_redirect https://lovemaple.heroku.com/ /blog/;
    
    # Rewrite response headers containing URLs
    proxy_cookie_path / /blog/;
    sub_filter_once off;
    sub_filter_types *;
    sub_filter 'href="/' 'href="/blog/';
    sub_filter 'src="/' 'src="/blog/';
    sub_filter 'url(/' 'url(/blog/';
}

1. Trailing Slashes Matter
Both location /blog/ and proxy_pass http://lovemaple.heroku.com/ must include trailing slashes to ensure proper path stripping.

2. Header Adjustments
The X-Forwarded-Prefix header helps applications detect they're behind a reverse proxy. Many frameworks (Express, Rails, Django) use this to generate correct URLs.

3. Response Rewriting
The sub_filter directives modify HTML/CSS responses to fix resource paths. For modern SPAs, you might need additional handling:

sub_filter '"/api"' '"/blog/api"';
sub_filter '"/assets"' '"/blog/assets"';

If your Heroku app uses WebSockets, add these parameters:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;

After applying changes, verify with:

sudo nginx -t
sudo systemctl restart nginx

Check these critical points in your browser:

  • All static resources load with /blog/ prefix
  • Navigation links preserve the prefix
  • Form submissions work correctly
  • Redirects maintain the subdirectory

If you control the Heroku application, configure it with:

# Express.js example
app.set('trust proxy', true);
app.use(express.static('public', { 
  setHeaders: (res) => {
    res.setHeader('X-Forwarded-Prefix', '/blog');
  }
}));

# Rails example
config.relative_url_root = "/blog"
config.action_controller.relative_url_root = "/blog"

This approach is more maintainable but requires access to the application source.

Examine requests with:

curl -v http://yoursite.com/blog
tail -f /var/log/nginx/error.log

Look for these headers in responses:

X-Forwarded-* headers
Location headers in redirects
Content-Location headers