How to Create an Nginx Reverse Proxy

Configure an Nginx reverse proxy with proxy_pass paths, TLS, WebSocket headers, load balancing, SELinux, and 502/504/404 fixes.

UpdatedPublished AuthorJoshua JamesRead time15 minGuide typeNginx

A reverse proxy becomes useful as soon as a local app needs a public domain, TLS, and one clean entry point. An Nginx reverse proxy gives private Node.js, Python, API, media-server, or internal web backends a public HTTP endpoint while those services stay bound to localhost or a private address.

Start by proving the backend directly. Then create an HTTP server block, test the response through Nginx, and add TLS, WebSocket support, load balancing, or hardening only where the application needs those layers. You need access to the active Nginx configuration files and permission to reload the service; if a hosting panel, CDN, Kubernetes ingress, Docker proxy manager, or load balancer owns the public edge, apply the equivalent proxy settings there instead.

Create an HTTP Nginx Reverse Proxy

Check Nginx and Backend Prerequisites

Confirm that Nginx is installed before you create the proxy configuration:

nginx -v

A normal version check prints the installed Nginx branch:

nginx version: nginx/1.x.x

If Nginx is not installed yet, use the guide for your server distribution:

You also need a backend service that already responds locally or on a private network address. Use the curl command to test the backend directly from the Nginx server before adding a proxy layer:

curl http://127.0.0.1:3000

If the backend returns the expected application response, continue with the Nginx configuration. If this direct request fails, fix the application listener first; the reverse proxy can only forward to a service that is already reachable from the Nginx host.

Map the Nginx Reverse Proxy Request Flow

A working reverse proxy needs each layer to answer the right request. Check the layer that fails before changing a production server:

StageWhat to verifyCommon failure
Client to NginxDNS, public IP, HTTP or HTTPS listener, and matching server_nameDefault site answers, wrong certificate, firewall block, or 404 before proxying
Nginx to backendproxy_pass scheme, host, port, path, and local network reachability502 from connection refused, wrong private IP, blocked SELinux policy, or closed backend port
Backend to NginxApplication status, response time, redirect targets, and trusted proxy settings504 from slow responses, redirects to HTTP, backend-generated 404, or wrong absolute URLs
Nginx to clientForwarded headers, TLS response, caching, compression, and security headersMixed-content redirects, missing client IP, stale cache, or browser policy errors

Apply reverse proxy changes during a maintenance window or with a tested rollback plan when the virtual host already serves production traffic. A wrong server_name, broad location, or broken upstream can affect every request for that site.

Match Nginx Proxy Directives to the Job

A reverse proxy configuration usually combines one routing directive with several header directives. These directives are the small set most examples depend on:

DirectiveContextDefault or version notePurpose
proxy_passlocation, if in location, limit_exceptNo defaultSends matching requests to an upstream URL, IP address, server group, or Unix socket.
proxy_set_headerhttp, server, locationDefaults include Host $proxy_host and Connection closeControls which request headers Nginx passes to the backend application.
proxy_http_versionhttp, server, locationNginx 1.29.7 and newer default to HTTP/1.1; older versions defaulted to HTTP/1.0Sets the upstream HTTP protocol version. Keeping 1.1 explicit helps older packages and WebSocket examples.
upstreamhttpNo defaultDefines a named backend group for load balancing or failover.
proxy_read_timeouthttp, server, locationDefaults to 60sControls how long Nginx waits between reads from the backend response.

Choose the Nginx Configuration File

Configuration include paths vary by distribution. Debian and Ubuntu commonly use /etc/nginx/sites-available/ with symlinks in /etc/nginx/sites-enabled/, while Fedora, RHEL, Rocky Linux, and similar systems commonly load .conf files from /etc/nginx/conf.d/. Both locations are normally included from the main http context.

Back up the main Nginx configuration before changing a working server:

sudo cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.backup

On Debian and Ubuntu, create a site file:

sudo nano /etc/nginx/sites-available/reverse-proxy

On Fedora, RHEL, Rocky Linux, and similar systems, create a file in conf.d instead:

sudo nano /etc/nginx/conf.d/reverse-proxy.conf

Choose the Backend Target for proxy_pass

The proxy_pass target should match where the application really listens from the Nginx server’s point of view. Choose the narrowest reachable target instead of exposing the backend publicly:

Backend targetExampleUse when
Local TCP listenerhttp://127.0.0.1:3000The app runs on the same server and listens only on loopback.
Private LAN listenerhttp://192.168.1.12:3000The app runs on another host reachable over a private subnet, VPN, or tailnet.
Named upstream grouphttp://backend_serversSeveral backends serve the same app and need load balancing or passive failover.
HTTPS upstreamhttps://backend.example.internal:8443The backend requires TLS on the private side or crosses a network where plain HTTP is not acceptable.
Unix sockethttp://unix:/run/myapp/app.sock:/The app exposes a local socket and the Nginx worker user can read and write that socket.

For a backend on another private host, test that exact address from the Nginx server before using it in proxy_pass:

curl http://192.168.1.12:3000

A failed private-address request means the problem is routing, firewall policy, backend binding, or the application service itself. Fix that path before adding Nginx, because a public reverse proxy cannot repair a backend that the Nginx host cannot reach.

Add the Basic Nginx Reverse Proxy Server Block

Start with an HTTP-only virtual host. This keeps the first working proxy separate from the later certificate and HTTPS redirect steps.

server {
    listen 80;
    server_name example.com www.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.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;
    }
}

Replace example.com with your domain and update proxy_pass to match the backend listener. Common backend targets include http://127.0.0.1:3000 for Node.js applications, http://127.0.0.1:8000 for Django or FastAPI, http://127.0.0.1:5000 for Flask, and a private address such as http://192.168.1.10:8080 when the application runs on another host.

Set Standard Nginx Reverse Proxy Headers

The header lines preserve request details that many applications need for redirects, logging, sessions, and client-IP handling:

  • Host $host passes the requested hostname instead of the internal upstream name.
  • X-Real-IP $remote_addr sends the direct client IP seen by Nginx.
  • X-Forwarded-For $proxy_add_x_forwarded_for appends the client IP to the forwarding chain.
  • X-Forwarded-Proto $scheme tells the backend whether the original request reached Nginx over HTTP or HTTPS.

Configure backend applications to trust forwarded headers only from known proxy IP addresses. If the backend trusts these headers from any source, a direct client can forge IP or protocol values and bypass application-side checks.

On Debian and Ubuntu systems, enable the site by linking it into sites-enabled:

sudo ln -s /etc/nginx/sites-available/reverse-proxy /etc/nginx/sites-enabled/

Systems that load /etc/nginx/conf.d/*.conf do not need the symlink step.

Test and Reload the Nginx Reverse Proxy

Test the Nginx syntax before applying the virtual host:

sudo nginx -t

A successful syntax test prints these lines:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

If Nginx reports an error, fix the file and line number shown in the message before reloading. Missing semicolons, unmatched braces, and directives placed in the wrong context are the most common causes.

Reload Nginx after the syntax test passes:

sudo systemctl reload nginx

The reload command normally returns no output on success. Unlike a restart, a reload asks Nginx to apply the new configuration while existing worker processes finish active requests.

Verify the Nginx Reverse Proxy Response

Send a request through Nginx with the expected host header:

curl -H "Host: example.com" http://127.0.0.1

The response should match the backend application. A JSON backend might return output like this:

{"status":"ok","message":"Application running"}

After DNS points to the server, check the public HTTP endpoint from another machine:

curl -I http://example.com

A successful header response proves that Nginx accepted the request and returned an HTTP response from the proxied site. Backend logs should show the forwarded host and client-IP headers when your application records them.

Route Paths with Nginx proxy_pass

The proxy_pass URI decides whether Nginx preserves the original request path or replaces the matching location prefix before sending the request upstream. Preserve the path when the backend already serves the public prefix; replace the prefix when Nginx is mounting the app under a different public path. Path handling is one of the most common causes of reverse-proxy 404 errors because a syntactically valid config can still ask the backend for the wrong URI.

Preserve the Original Request URI

Use an upstream address without a URI when the backend should receive the same path the client requested:

location /storage/ {
    proxy_pass http://127.0.0.1:3000;
}

With this form, a public request for /storage/image.png reaches the backend as /storage/image.png. This is usually best when the application already serves the same path prefix.

Replace a Public Path Prefix

Add a URI after the upstream address when the public path should map to a different backend path:

location /app/ {
    proxy_pass http://127.0.0.1:3000/;
}

With this form, a public request for /app/login reaches the backend as /login. To map one public prefix to another backend prefix, put the backend prefix in the URI:

location /files/ {
    proxy_pass http://127.0.0.1:3000/storage/;
}

With this mapping, /files/image.png reaches the backend as /storage/image.png. For a deeper explanation of slash behavior and replacement rules, use the Nginx proxy_pass trailing slash guide. When several location blocks might match the same request, check Nginx location block priority before adding another prefix or regex location.

Proxy to an HTTPS Backend

Public TLS termination and upstream TLS are separate decisions. Use https:// in proxy_pass only when the backend itself expects HTTPS, then preserve the normal forwarding headers and enable SNI when the backend certificate is name-based:

location / {
    proxy_pass https://backend.example.internal:8443;
    proxy_ssl_server_name on;
    proxy_ssl_name backend.example.internal;

    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;
}

Nginx does not verify proxied HTTPS certificates unless upstream verification is enabled. When the backend crosses an untrusted network, add a trusted CA bundle or internal CA file with proxy_ssl_trusted_certificate and enable proxy_ssl_verify on after confirming the CA path for your distribution.

Configure Nginx Reverse Proxy Load Balancing

Use an upstream block when more than one backend can serve the same application. The upstream block belongs in the http context, which is where standard site include files are usually loaded. For deeper load-balancing behavior, health-check boundaries, and upstream method comparisons, use the dedicated Nginx upstream load balancing guide.

Use Round-Robin Nginx Load Balancing

Nginx uses weighted round-robin load balancing by default. Servers with equal weights receive requests in a balanced sequence.

upstream backend_servers {
    server 192.168.1.10:3000 max_fails=3 fail_timeout=30s;
    server 192.168.1.11:3000 max_fails=3 fail_timeout=30s;
    server 192.168.1.12:3000 max_fails=3 fail_timeout=30s;
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://backend_servers;
        proxy_http_version 1.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;
    }
}

The max_fails and fail_timeout parameters provide passive failure handling. In this example, after three failed attempts within 30 seconds, Nginx temporarily avoids that backend for 30 seconds. Open source Nginx does not send active health probes with this syntax; active health_check behavior belongs to NGINX Plus.

Test the complete Nginx configuration, then reload the service:

sudo nginx -t && sudo systemctl reload nginx

Use Least Connections for Uneven Nginx Backends

The least_conn method sends a new request to the backend with the fewest active connections, while still considering server weights. It is useful when requests vary widely in duration.

upstream backend_servers {
    least_conn;

    server 192.168.1.10:3000 max_fails=3 fail_timeout=30s;
    server 192.168.1.11:3000 max_fails=3 fail_timeout=30s;
    server 192.168.1.12:3000 max_fails=3 fail_timeout=30s;
}

Use IP Hash for Basic Nginx Session Persistence

The ip_hash method keeps requests from the same client IP on the same backend unless that backend is unavailable. It helps simple applications that store sessions locally, but it is not a full replacement for shared session storage.

upstream backend_servers {
    ip_hash;

    server 192.168.1.10:3000;
    server 192.168.1.11:3000;
    server 192.168.1.12:3000;
}

IP hash is weak when many users share one public IP address, such as corporate NAT, VPN exits, or mobile carrier networks. For stronger persistence, store sessions in a shared system such as Redis or use application-level sticky-session logic.

Use Weighted Nginx Reverse Proxy Backends

Weights send proportionally more traffic to larger backends. A server with weight=3 receives roughly three times the requests of a server with weight=1 when all backends are available.

upstream backend_servers {
    server 192.168.1.10:3000 weight=3 max_fails=3 fail_timeout=30s;
    server 192.168.1.11:3000 weight=2 max_fails=3 fail_timeout=30s;
    server 192.168.1.12:3000 weight=1 max_fails=3 fail_timeout=30s;
    server 192.168.1.13:3000 backup;
}

The backup parameter marks a backend that receives traffic only when the primary servers are unavailable. Nginx documents that backup cannot be combined with some load-balancing methods, including ip_hash, so keep failover and persistence designs separate unless you have tested the exact combination.

Add TLS to an Nginx Reverse Proxy

TLS termination lets Nginx handle HTTPS at the public edge and forward plain HTTP to the private backend. Add TLS only after the HTTP reverse proxy works, because Certbot and redirect troubleshooting are much easier when the base virtual host already responds on port 80.

Obtain Certificates for the Nginx Reverse Proxy

Install Certbot and the Nginx plugin from your distribution’s supported package source. For distro-specific package names, renewal behavior, and firewall notes, use the dedicated guides for Let’s Encrypt with Nginx on Debian or Let’s Encrypt with Nginx on Ubuntu.

Once Certbot is installed and the HTTP server block responds for the domain, request certificates without asking Certbot to install the certificate into the final proxy configuration:

sudo certbot certonly --nginx -d example.com -d www.example.com

Certbot stores the certificate and private key under the domain directory in /etc/letsencrypt/live/:

  • /etc/letsencrypt/live/example.com/fullchain.pem contains the certificate chain.
  • /etc/letsencrypt/live/example.com/privkey.pem contains the private key.

Configure HTTPS for the Nginx Reverse Proxy

Add an HTTPS server block that points to the certificate files and keeps the same proxy headers as the HTTP version:

upstream backend_servers {
    server 192.168.1.10:3000 max_fails=3 fail_timeout=30s;
    server 192.168.1.11:3000 max_fails=3 fail_timeout=30s;
}

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

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    add_header Strict-Transport-Security "max-age=300" always;

    location / {
        proxy_pass http://backend_servers;
        proxy_http_version 1.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;
    }
}

HTTP/2 is optional for the reverse proxy. On Nginx 1.25.1 and later, add http2 on; inside the server block. Older packaged Nginx builds may still require listen 443 ssl http2; instead. Use the Nginx HTTP/2 configuration guide when protocol negotiation, package syntax, and validation output matter.

Start HSTS with a short max-age, verify every HTTPS route and rollback path, then increase the value gradually. Add includeSubDomains only when every subdomain under the hostname is ready for HTTPS. A global HSTS policy can break staging, mail, panels, or other subdomains that still need plain HTTP access.

Redirect HTTP to the HTTPS Nginx Reverse Proxy

After the HTTPS server block works, replace the original HTTP proxy with a redirect server block:

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

    return 301 https://$host$request_uri;
}

The $host variable keeps the requested hostname, while $request_uri keeps the path and query string. For canonical host redirects between www and non-www, use the dedicated redirect www or non-www in Nginx workflow instead of mixing host canonicalization into the reverse proxy example.

Test and reload Nginx after adding the redirect:

sudo nginx -t && sudo systemctl reload nginx

Verify the redirect and HTTPS response separately:

curl -I http://example.com
curl -I https://example.com

Relevant headers should include a permanent redirect on HTTP and a successful response on HTTPS:

HTTP/1.1 301 Moved Permanently
Location: https://example.com/

HTTP/1.1 200 OK

Configure Nginx Reverse Proxy WebSocket Support

WebSocket applications need upgrade headers because Upgrade and Connection are hop-by-hop headers. Nginx does not pass them to the backend unless you set them explicitly.

Place the map block in the http context, outside the server block:

map $http_upgrade $connection_upgrade {
    default upgrade;
    '' close;
}

Then use the mapped value in the WebSocket location:

upstream websocket_backend {
    server 192.168.1.10:3000;
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://websocket_backend;
        proxy_http_version 1.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;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        proxy_buffering off;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }
}

Nginx WebSocket proxying depends on these explicit upgrade headers. The extended timeouts keep idle WebSocket connections open longer than the default 60-second read timeout; adjust them to match the application’s heartbeat behavior rather than copying day-long values into every deployment. For handshake probes, WSS examples, and WebSocket-specific 400, 502, and 504 fixes, use the Nginx WebSocket proxy guide.

Harden an Nginx Reverse Proxy

Restrict Direct Access to Nginx Backends

Backend services should usually accept traffic only from the Nginx proxy host. That prevents clients from bypassing the proxy’s TLS, logging, rate limiting, and header controls.

On systems that use UFW, allow the backend port from the proxy IP:

sudo ufw allow proto tcp from 192.168.1.5 to any port 3000 comment 'Allow Nginx to backend'

On firewalld-based systems, add an equivalent rich rule:

FW_ZONE=$(sudo firewall-cmd --get-default-zone)
sudo firewall-cmd --zone="$FW_ZONE" --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.5" port port="3000" protocol="tcp" accept'
sudo firewall-cmd --reload
sudo firewall-cmd --zone="$FW_ZONE" --query-rich-rule='rule family="ipv4" source address="192.168.1.5" port port="3000" protocol="tcp" accept'

Replace 192.168.1.5 with the proxy server’s private IP address. If the backend interface belongs to a different active firewalld zone, use that zone instead of the default. Keep a console or out-of-band access path available before changing firewall rules on a remote server.

Allow SELinux Nginx Backend Connections

Fedora, RHEL, Rocky Linux, AlmaLinux, and CentOS Stream can run Nginx under SELinux policy that blocks outbound backend connections even when nginx -t succeeds. Red Hat documents the httpd_can_network_connect boolean for reverse proxy traffic.

Check SELinux mode and the current boolean value before changing it:

getenforce
getsebool httpd_can_network_connect

When SELinux is enforcing and the backend connection is blocked, enable outbound network connections for confined web server processes:

sudo setsebool -P httpd_can_network_connect 1
getsebool httpd_can_network_connect

The -P flag makes the boolean persistent. Disable it only when you enabled it for this reverse proxy and no other confined web service needs outbound network access:

sudo setsebool -P httpd_can_network_connect 0

Validate Nginx Proxy Headers in the Backend

Application frameworks need their own trusted-proxy setting before they should use X-Forwarded-* headers for client IP, protocol, or redirect decisions. In Express, trust only the proxy address rather than every incoming connection:

app.set('trust proxy', '192.168.1.5')

Avoid Open Nginx Proxy Configurations

Do not build a proxy_pass target from unvalidated user input. This pattern creates an open proxy that can forward requests to arbitrary external or internal targets:

location / {
    proxy_pass http://$arg_target;
}

If the upstream must be dynamic, validate the allowed targets in the application layer or map requests to a controlled set of named upstreams.

Rate Limit an Nginx Reverse Proxy Location

Rate limiting belongs at the Nginx layer when a backend endpoint can be abused before the application has a chance to reject the request. Define the shared zone in the http context, then apply it inside the protected location:

limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

server {
    listen 80;
    server_name example.com;

    location /api/ {
        limit_req zone=api_limit burst=20 nodelay;

        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.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;
    }
}

This example allows 10 requests per second per client IP with a burst of 20. For delay behavior, logging, dry-run testing, and status-code choices, use the full Nginx rate limiting guide.

Troubleshoot Nginx Reverse Proxy Errors

Fix 502 Bad Gateway from an Nginx Reverse Proxy

A 502 response means Nginx could not get a valid response from the backend. The version suffix in an error page, such as nginx/1.29.7 or nginx/1.24.0 (Ubuntu), identifies the Nginx build that returned the page; it does not identify the root cause. Confirm the status first:

curl -sSI http://example.com
HTTP/1.1 502 Bad Gateway

Test the backend listener from the Nginx server:

curl -sS http://127.0.0.1:3000

If the backend is not listening, check the port:

sudo ss -H -tlnp 'sport = :3000'

Relevant output should show the backend port in LISTEN state:

LISTEN 0 511 127.0.0.1:3000 0.0.0.0:*

If no process is listening, start or fix the application service. If the service listens on another address or port, update proxy_pass, test Nginx, and reload:

sudo nginx -t && sudo systemctl reload nginx

When the listener exists but Nginx still returns 502, use tail for log inspection and check the newest errors for connection failures, permission denials, or upstream protocol mismatches:

sudo tail -50 /var/log/nginx/error.log

Relevant log fragments often name the upstream connection failure:

connect() failed (111: Connection refused) while connecting to upstream

If the log mentions Permission denied on a Fedora or RHEL-family system, check SELinux before changing the backend port or disabling security controls. If the error path is broader than a simple reverse proxy, use the full Nginx 502 Bad Gateway troubleshooting guide.

Fix 504 Gateway Timeout from an Nginx Reverse Proxy

A 504 response means the backend connection was established, but Nginx waited too long between response reads. Confirm the response code first:

curl -sSI http://example.com
HTTP/1.1 504 Gateway Time-out

For endpoints that legitimately run longer, increase the proxy timeouts in the affected location:

location /reports/ {
    proxy_pass http://backend_servers;
    proxy_http_version 1.1;

    proxy_connect_timeout 60s;
    proxy_send_timeout 60s;
    proxy_read_timeout 300s;

    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;
}

Longer timeouts should match a known slow route, not hide a failing backend. If ordinary pages need very high timeout values, investigate database queries, upstream saturation, external API calls, or application worker limits.

Test and reload after changing timeouts, then repeat the request that timed out:

sudo nginx -t && sudo systemctl reload nginx
curl -sSI http://example.com/reports/

When the timeout appears only under load, compare Nginx logs with backend worker, database, and external API timing. Use the full Nginx 504 Gateway Timeout guide for deeper upstream isolation.

Fix 404 Not Found from an Nginx Reverse Proxy

A 404 can come from Nginx before proxying or from the backend after Nginx forwards the request. Check the response through the local Nginx listener with the expected host:

curl -sSI -H "Host: example.com" http://127.0.0.1/storage/
HTTP/1.1 404 Not Found

If the default site answers, the request probably matched the wrong server block. Check enabled files, server_name, and default-server settings, then test the full config:

sudo nginx -T | grep -n "server_name example.com"
sudo nginx -t

If the request reaches the backend but the backend returns 404, compare the direct backend path with the public path:

curl -sSI http://127.0.0.1:3000/storage/
curl -sSI http://127.0.0.1:3000/

A backend 404 usually means the proxy_pass URI is preserving, stripping, or replacing a path prefix differently than the application expects. Match the path-routing pattern to the public and backend prefixes, then use the Nginx proxy_pass trailing slash guide or the full Nginx 404 Not Found guide when static roots, aliases, or location priority are involved.

Fix Wrong Hostname or HTTP Redirects Behind Nginx

Wrong hostnames, bad absolute URLs, or redirects back to HTTP usually mean the backend is not receiving the original host or scheme. Keep these two headers in the proxy location:

proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;

Then verify the request path through Nginx:

curl -I -H "Host: example.com" http://127.0.0.1

If the application still generates the wrong URLs, configure its trusted-proxy setting so it accepts X-Forwarded-Proto and Host only from the Nginx server. Media servers, dashboards, and authentication apps often also need their public URL, base URL, or external URL set to the HTTPS address that users open in a browser.

Fix TLS Certificate Errors on an Nginx Reverse Proxy

Certificate errors usually come from a wrong file path, a missing chain, or a certificate that does not cover the requested hostname. Confirm the files referenced by the server block exist:

sudo ls -l /etc/letsencrypt/live/example.com/

Use openssl s_client to inspect the certificate served by Nginx:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -issuer -dates

The command should print the served certificate subject, issuer, and validity dates. Check that the certificate subject or subject alternative name covers the hostname and that Nginx uses fullchain.pem, not only cert.pem. Test and reload after correcting certificate paths:

sudo nginx -t && sudo systemctl reload nginx

Fix Nginx Service Start Failures

Systemd often describes Nginx as A high performance web server and a reverse proxy server in service errors. That phrase is the unit description, not the root cause. Start with the Nginx syntax test because it usually names the exact file and line:

sudo nginx -t

If the syntax test passes but the service still fails, inspect systemd status and recent Nginx logs:

sudo systemctl status nginx --no-pager
sudo journalctl -u nginx -n 50 --no-pager

Common start failures include another process already listening on ports 80 or 443, a missing certificate file, a bad include path, or a directive that the installed Nginx build does not support. After fixing the specific error, retest and start or reload the service:

sudo nginx -t
sudo systemctl start nginx

Remove an Nginx Reverse Proxy Configuration

Remove a reverse proxy only after confirming another server block, upstream service, or maintenance page should handle the traffic. Removing the wrong site file can take the domain offline.

These commands remove Nginx configuration files or symlinks. Confirm the filenames belong to the reverse proxy site before running them on a production server.

On Debian and Ubuntu, remove the enabled symlink first:

sudo rm /etc/nginx/sites-enabled/reverse-proxy

If you no longer need the saved site file, remove it separately:

sudo rm /etc/nginx/sites-available/reverse-proxy

On systems that load conf.d files directly, remove the proxy file:

sudo rm /etc/nginx/conf.d/reverse-proxy.conf

Test the remaining configuration and reload Nginx:

sudo nginx -t && sudo systemctl reload nginx

If you added a source-restricted backend firewall rule for this proxy, remove only that rule. On UFW systems, use the same rule shape with delete:

sudo ufw delete allow proto tcp from 192.168.1.5 to any port 3000

On firewalld systems, remove the matching rich rule from the same zone:

FW_ZONE=$(sudo firewall-cmd --get-default-zone)
sudo firewall-cmd --zone="$FW_ZONE" --permanent --remove-rich-rule='rule family="ipv4" source address="192.168.1.5" port port="3000" protocol="tcp" accept'
sudo firewall-cmd --reload

Certificates under /etc/letsencrypt/ are not removed with the Nginx site file. If the certificate is no longer used by any hostname, remove it with Certbot:

sudo certbot delete --cert-name example.com

Conclusion

A solid Nginx reverse proxy starts with a reachable backend, then adds tested routing, path handling, TLS, WebSocket support, backend access controls, and cleanup ownership. Useful next steps are Nginx security headers, Nginx proxy caching, or Nginx access and error logs when you need deeper operational visibility.

Share this guide

Help another Linux user troubleshoot faster

Share this guide with someone troubleshooting Linux systems or saving it for later.

Follow LinuxCapable

Want more LinuxCapable guides in Google?

Add LinuxCapable as a preferred source so Google can show more of our fresh Linux tutorials in Top Stories and From your sources when relevant.

Add LinuxCapable as a preferred source on Google
Search LinuxCapable

Need another guide?

Search LinuxCapable for package installs, commands, troubleshooting, and follow-up guides related to what you just read.

Found this guide useful?

Support LinuxCapable to keep tutorials free and up to date.

Buy me a coffeeBuy me a coffee
Before commenting, please review our Comments Policy.
Formatting tips for your comment

You can use basic HTML to format your comment. Useful tags currently allowed in published comments:

You type Result
<code>command</code> command
<strong>bold</strong> bold
<em>italic</em> italic
<blockquote>quote</blockquote> quote block

Got a Question or Feedback?

We read and reply to every comment - let us know how we can help or improve this guide.

Let us know you are human: