Web Server Intermediate

Fixing Nginx proxy_pass Trailing Slash Resolution for Subfolders on Ubuntu 22.04 LTS

Resolve Nginx proxy_pass subfolder routing issues caused by trailing slash discrepancies on Ubuntu 22.04 LTS. Correctly configure proxy rules for seamless application access.

👨‍💻
Senior Systems Architect • Verified in Staging Labs

Resolve Nginx proxy_pass subfolder routing issues caused by trailing slash discrepancies on Ubuntu 22.04 LTS. Correctly configure proxy rules for seamless application access.

This guide provides a comprehensive solution for troubleshooting and resolving common Nginx proxy_pass issues related to trailing slashes when configuring a reverse proxy for a subfolder application on Ubuntu 22.04 LTS. As an expert Systems Administrator, you'll understand that subtle differences in URI processing can lead to frustrating 404 errors, incorrect content display, or even redirect loops. We will dissect the underlying mechanics and provide precise configuration examples to ensure your backend applications are correctly served through Nginx.

Symptom & Error Signature

Users attempting to access a web application proxied through Nginx via a subfolder path (e.g., https://example.com/my-app/) may encounter one of the following symptoms:

  1. HTTP 404 Not Found: The most common symptom. Nginx successfully proxies the request, but the backend application responds with a 404, indicating it couldn't find the requested resource. This often happens because the URI passed to the backend is incorrect (e.g., /my-app/index.html instead of /index.html).
  2. Incorrect Content or Malfunctioning Application: The application loads partially, displays broken links (relative paths resolving incorrectly), or interactive elements fail due to misrouted API calls.
  3. Redirect Loops: The browser continuously redirects between paths, often involving /my-app and /my-app/, or between different paths on the backend, leading to a "Too many redirects" error.

While there isn't a single "error signature" in Nginx logs for this misconfiguration, typical access logs might show the Nginx server successfully forwarding the request, followed by the backend returning a 404 status. For example, a curl -v command might reveal:

$ curl -v https://example.com/my-app/dashboard
*   Trying 203.0.113.10:443...
* Connected to example.com (203.0.113.10) port 443 (#0)
... (SSL handshake) ...
> GET /my-app/dashboard HTTP/1.1
> Host: example.com
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark large easy handle as doing multi (no longer waiting for background connect)
* We are currently in DO_DONE state
< HTTP/1.1 404 Not Found
< Server: Kestrel
< Date: Thu, 13 Aug 2026 10:00:00 GMT
< Content-Length: 0
<
* Connection #0 to host example.com left intact

In this scenario, Nginx forwarded /my-app/dashboard to the backend, which expected just /dashboard and responded with a 404.

Root Cause Analysis

The core of this problem lies in how Nginx processes and rewrites Uniform Resource Identifiers (URIs) based on the presence or absence of a trailing slash in both the location block and the proxy_pass directive. Understanding this behavior is critical.

Nginx's proxy_pass directive has two distinct behaviors concerning URI manipulation:

  1. proxy_pass with a trailing slash (e.g., proxy_pass http://backend/;):

    • When the proxy_pass directive itself ends with a trailing slash, Nginx strips the portion of the URI matched by the location block from the client request. The remaining part of the URI is then appended to the proxy_pass URL.
    • Example:
      • location /my-app/ { proxy_pass http://backend:8080/; }
      • Client request: GET /my-app/dashboard
      • URI sent to backend: GET /dashboard (The /my-app/ prefix is stripped)
    • This is typically desired when your backend application expects to be served from its root path (i.e., it doesn't know or care about the /my-app subfolder prefix).
  2. proxy_pass without a trailing slash (e.g., proxy_pass http://backend;):

    • When the proxy_pass directive does not end with a trailing slash, Nginx passes the entire URI portion that matched the location block (including the prefix) to the backend server.
    • Example:
      • location /my-app/ { proxy_pass http://backend:8080; }
      • Client request: GET /my-app/dashboard
      • URI sent to backend: GET /my-app/dashboard (The entire matched URI is passed)
    • This is useful if your backend application is configured to handle requests with the subfolder prefix (e.g., a multi-tenant application where /my-app/ maps to a specific tenant).

Interaction with location blocks:

  • A location /subfolder/ block matches requests that explicitly include the trailing slash (e.g., /subfolder/foo). Nginx will often internally redirect /subfolder to /subfolder/ if a location for the directory /subfolder/ exists.
  • A location /subfolder block (without a trailing slash) typically matches requests that start with /subfolder, but the exact matching behavior can be tricky, especially with exact matches (=) or regular expressions (~). For subfolder proxying, using location /subfolder/ or a regex location ~ ^/subfolder/ is generally more predictable.

The "resolving error" commonly occurs because the Nginx configuration's proxy_pass directive (and its trailing slash status) does not align with what the backend application expects regarding the URI path.

Step-by-Step Resolution

Follow these steps to correctly configure your Nginx proxy_pass directive for subfolder applications on Ubuntu 22.04 LTS.

1. Identify Your Backend's Expected URI Path

Before making any changes, determine whether your backend application expects requests to its root (/) or to a specific subpath (/my-app/).

  • Most common scenario: Backend expects requests relative to its root (e.g., GET /dashboard). This means Nginx needs to strip the subfolder prefix.
  • Less common scenario: Backend is aware of the subfolder and expects requests to include it (e.g., GET /my-app/dashboard). This means Nginx needs to preserve the subfolder prefix.

2. Access Your Nginx Configuration

Your Nginx virtual host configuration files are typically located in /etc/nginx/sites-available/.

sudo nano /etc/nginx/sites-available/your_domain.conf

Replace your_domain.conf with the actual name of your Nginx configuration file.

3. Configure Nginx location and proxy_pass Directives

Based on your backend's requirements (identified in Step 1), apply one of the following configurations. We recommend using a location block that ends with a trailing slash for consistency.

Option A: Backend expects requests relative to its root (most common)

If your backend application expects the URI path to not include the subfolder prefix (e.g., a request to https://example.com/my-app/dashboard should translate to http://backend:8080/dashboard), use proxy_pass with a trailing slash:

server {
    listen 80;
    listen [::]:80;
    server_name example.com;

    return 301 https://$host$request_uri; # Redirect HTTP to HTTPS
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # Your SSL cert
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # Your SSL key
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # Serve static files for the root domain if needed
    root /var/www/html;
    index index.html index.htm;

    # Configuration for the subfolder application
    location /my-app/ {
        # Proxy to the backend, stripping the /my-app/ prefix
        proxy_pass http://127.0.0.1:8080/; # <--- IMPORTANT: Note the trailing slash here!
        
        # Standard proxy headers
        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;
        
        # Adjust for websockets if needed
        # proxy_http_version 1.1;
        # proxy_set_header Upgrade $http_upgrade;
        # proxy_set_header Connection "upgrade";
        
        # Prevent Nginx from intercepting error pages from the backend
        proxy_intercept_errors off;
    }

    # Optional: Redirect non-trailing-slash /my-app to /my-app/
    # This location handles requests to /my-app (without trailing slash)
    # and redirects them to /my-app/ to ensure consistent behavior
    location = /my-app {
        return 301 $scheme://$host/my-app/;
    }

    # Other locations...
}

When proxy_pass ends with a trailing slash (http://127.0.0.1:8080/;), Nginx strips the part of the URI matched by the location directive (/my-app/) before sending it to the backend. A request to /my-app/dashboard will be forwarded as /dashboard. This is ideal for backend applications unaware of the subfolder prefix.

Option B: Backend expects requests to include the full subfolder path

If your backend application is designed to receive the full subfolder path (e.g., a request to https://example.com/my-app/dashboard should translate to http://backend:8080/my-app/dashboard), use proxy_pass without a trailing slash:

server {
    # ... (same server block header as above, including SSL) ...

    # Configuration for the subfolder application
    location /my-app/ {
        # Proxy to the backend, preserving the /my-app/ prefix
        proxy_pass http://127.0.0.1:8080; # <--- IMPORTANT: Note NO trailing slash here!
        
        # Standard proxy headers
        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;
        
        # proxy_http_version 1.1;
        # proxy_set_header Upgrade $http_upgrade;
        # proxy_set_header Connection "upgrade";
        
        proxy_intercept_errors off;
    }

    # Optional: Redirect non-trailing-slash /my-app to /my-app/
    location = /my-app {
        return 301 $scheme://$host/my-app/;
    }

    # Other locations...
}

When proxy_pass does not end with a trailing slash (http://127.0.0.1:8080;), Nginx passes the entire matched URI (including the location prefix, /my-app/) to the backend. A request to /my-app/dashboard will be forwarded as /my-app/dashboard. This is suitable for backend applications that expect to handle the subfolder path themselves.

4. Test Nginx Configuration and Reload

After making changes to your Nginx configuration file, it's crucial to test for syntax errors before reloading the service.

sudo nginx -t

You should see nginx: the configuration file /etc/nginx/nginx.conf syntax is ok and nginx: configuration file /etc/nginx/nginx.conf test is successful.

If there are any errors, Nginx will point them out, and you must correct them before proceeding.

Once the test is successful, reload Nginx to apply the new configuration:

sudo systemctl reload nginx

5. Verify the Solution

Open your web browser and try accessing your subfolder application (e.g., https://example.com/my-app/). Also, test accessing the path without a trailing slash (e.g., https://example.com/my-app) to ensure the optional redirect works as expected.

Use curl -v again to observe the HTTP headers and status codes, verifying that requests are now correctly reaching your backend and receiving appropriate responses (e.g., HTTP/1.1 200 OK).

curl -v https://example.com/my-app/dashboard

If your backend application itself issues redirects (e.g., from / to /login), ensure these redirects are correctly handled and do not result in absolute URLs that omit the /my-app/ prefix. Sometimes, additional proxy_redirect directives might be needed for complex backend redirect scenarios, though Nginx handles many common cases automatically. For instance: proxy_redirect http://backend:8080/ /my-app/;. This tells Nginx to rewrite Location headers from the backend.

👨‍💻

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.