Web Server Intermediate

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.

👨‍💻
Senior Systems Architect • Verified in Staging Labs

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:

  1. 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's dashboard route should be accessible.
  2. 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.
  3. Unexpected Redirects: The backend service might issue a redirect (e.g., a 301 or 302) to an incorrect or non-existent path.
  4. Nginx Access Logs: You might observe requests in Nginx's access.log being forwarded with an incorrect URI to the backend. For example, if you expect /dashboard but 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.

  1. location /app/ { proxy_pass http://backend_server:8080/; } (Both have trailing slash):

    • Nginx takes the part of the URI after the location path.
    • Request: http://yourdomain.com/app/dashboard
    • Nginx removes /app/ from the request, leaving dashboard.
    • Nginx appends dashboard to http://backend_server:8080/.
    • Backend receives: http://backend_server:8080/dashboard (Often desired behavior)
  2. location /app/ { proxy_pass http://backend_server:8080; } (Location has slash, proxy_pass lacks slash):

    • This is a common pitfall! Nginx replaces the location path with the proxy_pass URI. It doesn't strip the prefix.
    • Request: http://yourdomain.com/app/dashboard
    • Nginx replaces /app/ with http://backend_server:8080.
    • Backend receives: http://backend_server:8080/app/dashboard (Leads to 404 if backend expects /dashboard)
  3. 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 appends dashboard to http://backend_server:8080/.
    • Backend receives: http://backend_server:8080/dashboard (Still often desired, but can be confusing for regex location blocks).
  4. location /app { proxy_pass http://backend_server:8080; } (Both lack trailing slash):

    • Nginx passes the full matched URI including the location path.
    • 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)

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 location block and the proxy_pass URI when you intend to strip the path prefix. Using a location /app/ with proxy_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_pass inside a regex location block, avoid adding a URI part directly to proxy_pass (e.g., proxy_pass http://backend/uri/). Instead, explicitly append the rewritten URI using variables like $1, $request_uri, or $uri. If proxy_pass does contain a URI in a regex location, the behavior becomes more complex and can be unexpected, potentially passing both the regex match and the proxy_pass URI.

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.

  1. Test Nginx Configuration:

    sudo nginx -t
    

    This 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 successful
    
  2. Reload Nginx Service: If the configuration test is successful, reload Nginx to apply the changes without dropping active connections.

    sudo systemctl reload nginx
    
  3. Verify 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 Nginx access.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 -t before sudo systemctl reload nginx to prevent service downtime due to misconfigurations. If Nginx fails to start or reload, check /var/log/nginx/error.log for 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.

👨‍💻

Johnathon Wheeler

Senior Systems Architect & DevOps Engineer • Austin, TX

Connect on LinkedIn →

Johnathon has over 16 years of hands-on experience designing, debugging, and scaling Linux web hosting stacks, container clusters, and high-availability database architectures. Every guide on ButItWorkedLocal is independently tested against Debian 12, Ubuntu 24.04/22.04 LTS, Rocky Linux, and Docker environments to guarantee reproducibility in production.

🛡️

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.