When Nginx Reverse‑Proxy Returns 502 on Your Home‑Lab HTTPS Site: A Step‑by‑Step Fix for Certificate and Path Errors

502 from Nginx? Let’s Fix the HTTPS Path & Cert Issues

A 502 Bad Gateway is the first thing you’ll see when you expose a service behind Nginx in a home‑lab.
It usually means the proxy can’t talk to the backend – often because the SSL certificate is wrong, the upstream URL is mis‑typed, or the upstream host is unreachable.
Below is a step‑by‑step walk‑through that covers the most common pitfalls, shows how to verify each piece, and explains the trade‑offs you’ll face when choosing a solution.


1. Re‑check the Nginx Configuration

The first place to look is the Nginx block that proxies to your backend.
A minimal HTTPS reverse‑proxy looks like this:

server {
    listen 443 ssl;
    server_name example.local;

    ssl_certificate     /etc/ssl/certs/example.local.crt;
    ssl_certificate_key /etc/ssl/private/example.local.key;

    location / {
        proxy_pass https://backend.local:8443;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Common mistakes

Symptom Likely cause Fix
502 Bad Gateway proxy_pass points to http:// but backend only speaks HTTPS Change to https://
502 Backend URL has a typo (e.g., backend.local:8443 vs backend.local:8444) Correct the host/port
502 SSL cert on Nginx is for a different domain Use a cert that matches server_name or add server_name _; for a catch‑all
502 proxy_ssl_verify is true but the backend cert is self‑signed Disable verification for local traffic or add the CA to Nginx’s trust store

Tip: After editing the file, always test the syntax first:

sudo nginx -t

If the test passes, reload:

sudo systemctl reload nginx

2. Verify the Backend is Running

A 502 can also mean “Nginx can’t reach the backend at all.”
Use curl from the Nginx host to confirm the service is listening on the expected port.

curl -kv https://backend.local:8443

If you see a Connection refused or a timeout, the backend isn’t listening or a firewall is blocking it.

Common backend issues

Symptom Likely cause Fix
Connection refused Service not started sudo systemctl status myapp
Connection timed out Firewall on the backend host sudo ufw allow 8443/tcp
SSL handshake failed Backend cert is wrong or expired Regenerate cert or update CA bundle

If the backend is a container, double‑check the port mapping:

docker ps | grep myapp

Make sure the container exposes the correct port to the host.


3. Inspect the SSL Certificates

Nginx will refuse to serve HTTPS if the certificate chain is incomplete or the key doesn’t match the cert.

openssl x509 -in /etc/ssl/certs/example.local.crt -noout -text | grep "Subject:"
openssl rsa -in /etc/ssl/private/example.local.key -noout -text | grep "Public Key:"

The subject of the cert must match the server_name.
If you’re using a self‑signed cert, add the CA to Nginx’s trusted store:

sudo cp /etc/ssl/certs/example.local.crt /etc/ssl/certs/
sudo update-ca-certificates

Trade‑off:
Using a self‑signed cert keeps costs down and is fine for internal traffic, but browsers will warn users.
If you expose the site to the internet, consider Let’s Encrypt (certbot) or a paid CA.


4. Check the Backend’s TLS Configuration

When Nginx proxies to an HTTPS backend, it performs its own TLS handshake.
If the backend uses an older protocol (e.g., TLS 1.0) or a cipher that Nginx disallows, the handshake fails and Nginx returns 502.

openssl s_client -connect backend.local:8443 -tls1_2

If you get a handshake failure, update the backend to support modern TLS.
Most modern stacks (Node.js ≥ 18, Go ≥ 1.20, Python 3.12) default to TLS 1.2+.

Tip: In Nginx, you can relax the TLS version for the upstream:

proxy_ssl_protocols TLSv1.2 TLSv1.3;

But only do this if you’re sure the backend is secure.


5. Review the Nginx Error Log

The error log is the most reliable source of clues.
Open it in a new terminal while reproducing the 502:

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

Typical entries:

[error] 12345#0: *1 SSL_do_handshake() failed (SSL: error:14094418:SSL routines:ssl3_read_bytes:tlsv1 alert unknown ca) while SSL handshaking, client: 192.168.1.10, server: example.local, request: "GET / HTTP/1.1"

This line tells you the exact TLS error.
If you see unknown ca, the backend’s cert isn’t trusted by Nginx.
If you see handshake failure, the cipher mismatch is the culprit.


6. Test the Full Path with curl and openssl

Sometimes the 502 is caused by a missing path or wrong host header.
Add the -H "Host: example.local" header to mimic a real browser:

curl -kv https://example.local -H "Host: example.local"

If the backend expects a specific path (e.g., /api), adjust the location block:

location /api/ {
    proxy_pass https://backend.local:8443/;
}

Note the trailing slash – it tells Nginx to rewrite the request URI correctly.


7. Common “Path” Pitfalls

Symptom Likely cause Fix
502 when accessing / but works on /api Backend only serves /api Add a redirect or proxy for /
502 after adding a trailing slash Nginx rewrites URI incorrectly Use proxy_pass https://backend.local:8443; (no slash)
502 on sub‑domains server_name doesn’t match Add server_name *.example.local;

8. Use systemd Socket Activation (Optional)

If you’re running the backend as a systemd service, you can let systemd listen on the port and Nginx proxy to the socket.
This removes the need to expose a TCP port and eliminates the “port mismatch” issue.

# /etc/systemd/system/backend.socket
[Socket]
ListenStream=8443
Accept=yes

[Install]
WantedBy=sockets.target
# /etc/systemd/system/backend.service
[Service]
ExecStart=/usr/bin/myapp --listen /run/backend.sock

Then in Nginx:

location / {
    proxy_pass http://unix:/run/backend.sock;
}

Trade‑off:
Socket activation is cleaner and more secure, but it requires systemd support and a bit more setup.


9. Harden the Setup

Once the 502 is gone, consider these hardening steps:

Step Why How
Disable HTTP/1.0 Prevent old clients http2 on; and proxy_http_version 1.1;
Enable HSTS Force HTTPS add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Rate limit Mitigate DoS limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
Log only necessary fields Reduce disk usage log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent';

10. Quick Checklist for Future 502s

  1. Nginx syntax (nginx -t)
  2. Backend reachable (curl -kv https://backend.local:8443)
  3. Certificates match (openssl x509 -in …)
  4. TLS versions & ciphers (openssl s_client …)
  5. Error log (tail -f /var/log/nginx/error.log)
  6. Path correctness (proxy_pass slash rules)

If you hit a 502 again, run through this list in order. Most of the time you’ll spot the culprit within a few minutes.


11. Real‑world Example

I had a Raspberry Pi running a Flask app on port 5000.
My Nginx reverse‑proxy was configured for HTTPS on port 443, but the proxy_pass mistakenly pointed to http://localhost:5000 while the Flask app was listening on https://localhost:5000.
The result: a 502 that only appeared when I accessed the site over HTTPS.

Fix steps:

# 1. Update Nginx
sed -i 's|http://localhost:5000|https://localhost:5000|' /etc/nginx/sites-available/default

# 2. Test
sudo nginx -t && sudo systemctl reload nginx

# 3. Verify
curl -kv https://mypi.local

Now the site loads without a 502, and the browser shows a valid cert.


12. When to Ask for Help

If you’ve exhausted the checklist and still see a 502, consider:

  • Community forums – e.g., the Ubuntu Forums or Arch Linux Wiki.
  • GitHub issues – if the backend is an open‑source project, the issue tracker may have a similar problem.
  • Local meetup – many homelab enthusiasts share knowledge in person.

See also