Fixing Nginx proxy_pass Trailing Slash Subfolder Resolution Errors on CentOS Stream / Rocky Linux
Resolve common Nginx proxy_pass issues on CentOS Stream and Rocky Linux where incorrect trailing slash handling leads to 404s or misrouted requests. Learn precise configuration.
Resolve common Nginx proxy_pass issues on CentOS Stream and Rocky Linux where incorrect trailing slash handling leads to 404s or misrouted requests. Learn precise configuration.
Nginx's proxy_pass directive is a powerful tool for reverse proxying, but its interaction with trailing slashes in location blocks and the proxy_pass URI itself can be a frequent source of frustration. This guide addresses common misconfigurations that lead to incorrect subfolder resolution, resulting in 404 errors, unexpected content, or broken applications when proxying to backend services on CentOS Stream and Rocky Linux environments.
Symptom & Error Signature
Users typically experience one of the following symptoms:
- 404 Not Found Errors: When accessing a subfolder path like
https://yourdomain.com/app/dashboard, the browser displays a 404 page, even though the backend application'sdashboardroute should be accessible. - Incorrect Content/Broken Assets: The main page loads, but CSS, JavaScript, images, or API calls fail to load because their paths are resolved incorrectly by the backend server.
- Unexpected Redirects: The backend service might issue a redirect (e.g., a 301 or 302) to an incorrect or non-existent path.
- Nginx Access Logs: You might observe requests in Nginx's
access.logbeing forwarded with an incorrect URI to the backend. For example, if you expect/dashboardbut the backend is receiving/app/dashboard.
Example Nginx Access Log entry showing an incorrect backend URI (if logged by the backend):
192.168.1.1 - - [16/Oct/2026:10:30:00 +0000] "GET /app/dashboard HTTP/1.1" 404 153 "-" "Mozilla/5.0 (...)"
The key here is that the Nginx itself might not show an error, but the backend's response code (e.g., 404) indicates the issue. Nginx's error.log usually remains silent unless there's a connectivity issue to the backend.
Root Cause Analysis
The core of this problem lies in Nginx's precise (and often misunderstood) logic for constructing the URI sent to the backend when proxy_pass is used within a location block. The presence or absence of a trailing slash in both the location path and the proxy_pass URI dictates how Nginx rewrites the request URI.
Let's illustrate with an example where http://backend_server:8080/ is our target backend.
location /app/ { proxy_pass http://backend_server:8080/; }(Both have trailing slash):- Nginx takes the part of the URI after the
locationpath. - Request:
http://yourdomain.com/app/dashboard - Nginx removes
/app/from the request, leavingdashboard. - Nginx appends
dashboardtohttp://backend_server:8080/. - Backend receives:
http://backend_server:8080/dashboard(Often desired behavior)
- Nginx takes the part of the URI after the
location /app/ { proxy_pass http://backend_server:8080; }(Location has slash, proxy_pass lacks slash):- This is a common pitfall! Nginx replaces the
locationpath with theproxy_passURI. It doesn't strip the prefix. - Request:
http://yourdomain.com/app/dashboard - Nginx replaces
/app/withhttp://backend_server:8080. - Backend receives:
http://backend_server:8080/app/dashboard(Leads to 404 if backend expects/dashboard)
- This is a common pitfall! Nginx replaces the
location /app { proxy_pass http://backend_server:8080/; }(Location lacks slash, proxy_pass has slash):- This combination generally works, but it's less explicit.
- If request is
http://yourdomain.com/app, Nginx implicitly adds a trailing slash to the location match, effectively becoming/app/. Then it behaves like case 1. - If request is
http://yourdomain.com/app/dashboard, Nginx matches/app, removes it, and appendsdashboardtohttp://backend_server:8080/. - Backend receives:
http://backend_server:8080/dashboard(Still often desired, but can be confusing for regexlocationblocks).
location /app { proxy_pass http://backend_server:8080; }(Both lack trailing slash):- Nginx passes the full matched URI including the
locationpath. - Request:
http://yourdomain.com/app/dashboard - Backend receives:
http://backend_server:8080/app/dashboard(Desired if the backend itself uses/app/as its base path)
- Nginx passes the full matched URI including the
The key takeaway is that if proxy_pass has a URI component (anything after the hostname/port), Nginx considers it a fixed prefix that replaces the matched location part. If proxy_pass has no URI component (just http://hostname:port), Nginx appends the entire request URI (or the part matched by a regular expression location) to the backend.
Step-by-Step Resolution
The solution involves carefully crafting your location and proxy_pass directives to ensure the backend receives the expected URI. We'll cover the most common scenarios.
1. Standard Subfolder Proxying (Stripping the Prefix)
This is the most common scenario: You want https://yourdomain.com/app/ to map to the root (/) of your backend application.
Incorrect Configuration (Common Mistake):
# /etc/nginx/conf.d/yourdomain.conf
server {
listen 80;
listen [::]:80;
server_name yourdomain.com www.yourdomain.com;
location /app/ {
proxy_pass http://127.0.0.1:8080; # <-- NO TRAILING SLASH HERE
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;
}
}
With this, a request to yourdomain.com/app/dashboard would result in the backend receiving /app/dashboard, likely causing a 404.
Correct Configuration:
Ensure both the location path and the proxy_pass URI end with a trailing slash.
# /etc/nginx/conf.d/yourdomain.conf
server {
listen 80;
listen [::]:80;
server_name yourdomain.com www.yourdomain.com;
location /app/ {
proxy_pass http://127.0.0.1:8080/; # <-- ADD TRAILING SLASH HERE
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;
}
}
Now, yourdomain.com/app/dashboard will send http://127.0.0.1:8080/dashboard to the backend.
Always ensure consistent trailing slash usage between the
locationblock and theproxy_passURI when you intend to strip the path prefix. Using alocation /app/withproxy_pass http://backend/is the standard and most predictable way to achieve this.
2. Preserving the Subfolder Path
If your backend application is configured to handle the subfolder path (e.g., it expects requests to /app/dashboard), you need to preserve the path.
Correct Configuration:
Ensure neither the location path nor the proxy_pass URI ends with a trailing slash.
# /etc/nginx/conf.d/yourdomain.conf
server {
listen 80;
listen [::]:80;
server_name yourdomain.com www.yourdomain.com;
location /app { # <-- NO TRAILING SLASH
proxy_pass http://127.0.0.1:8080; # <-- NO TRAILING SLASH
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;
}
}
With this setup, yourdomain.com/app/dashboard will send http://127.0.0.1:8080/app/dashboard to the backend. This works because Nginx appends the entire URI matched by the location block to the proxy_pass destination.
3. Using Regex Location Blocks for More Control
Regular expression location blocks offer more granular control over URI rewriting. When using regex location blocks (e.g., location ~ ^/app/(.*)$), the proxy_pass directive must not include a URI part. Instead, you capture the desired part of the URI and append it to proxy_pass.
Correct Configuration with Regex:
# /etc/nginx/conf.d/yourdomain.conf
server {
listen 80;
listen [::]:80;
server_name yourdomain.com www.yourdomain.com;
location ~ ^/app/(.*)$ { # Capture everything after /app/
proxy_pass http://127.0.0.1:8080/$1; # Append the captured group ($1)
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;
}
# Optional: Handle the root /app itself if it's separate
location = /app {
return 301 /app/; # Enforce trailing slash for consistency if needed
}
location = /app/ {
proxy_pass http://127.0.0.1:8080/;
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;
}
}
Here, location ~ ^/app/(.*)$ captures dashboard from /app/dashboard into $1. proxy_pass http://127.0.0.1:8080/$1; then correctly sends /dashboard to the backend.
When using
proxy_passinside a regexlocationblock, avoid adding a URI part directly toproxy_pass(e.g.,proxy_pass http://backend/uri/). Instead, explicitly append the rewritten URI using variables like$1,$request_uri, or$uri. Ifproxy_passdoes contain a URI in a regexlocation, the behavior becomes more complex and can be unexpected, potentially passing both the regex match and theproxy_passURI.
4. Applying Changes and Reloading Nginx
After modifying your Nginx configuration files (typically in /etc/nginx/nginx.conf or /etc/nginx/conf.d/*.conf on CentOS Stream / Rocky Linux), you need to test the configuration and then reload Nginx.
Test Nginx Configuration:
sudo nginx -tThis command checks for syntax errors in your Nginx configuration. If there are no errors, you'll see a message like:
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successfulReload Nginx Service: If the configuration test is successful, reload Nginx to apply the changes without dropping active connections.
sudo systemctl reload nginxVerify the Fix: Access your subfolder path (e.g.,
https://yourdomain.com/app/) in your browser and check if the content loads correctly. You can also monitor your Nginxaccess.log(sudo tail -f /var/log/nginx/access.log) and your backend application's logs to confirm the URIs being processed.
Always use
sudo nginx -tbeforesudo systemctl reload nginxto prevent service downtime due to misconfigurations. If Nginx fails to start or reload, check/var/log/nginx/error.logfor details.
By understanding the subtle but critical role of trailing slashes in Nginx's location and proxy_pass directives, you can accurately configure your reverse proxies and avoid common subfolder resolution issues on CentOS Stream and Rocky Linux.
Our Production Verification Guarantee
Encountering a bug not covered here or running a non-standard kernel configuration? Our solutions are continually refined against real production incidents. Submit an environment trace for our editorial team to replicate.