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/
andproxy_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