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
- Nginx syntax (
nginx -t) - Backend reachable (
curl -kv https://backend.local:8443) - Certificates match (
openssl x509 -in …) - TLS versions & ciphers (
openssl s_client …) - Error log (
tail -f /var/log/nginx/error.log) - Path correctness (
proxy_passslash 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
- How to audit stale SSH keys with ssh-keygen‑l and fail2ban to stop brute‑force leaks on production servers
- Automating Let's‑Encrypt renewal on a single‑node web server without downtime
- When My Ubuntu 24.04 VPS Runs Out of Disk: A Step‑by‑Step Cleanup Script That Keeps Your Apache Logs from Failing
- When a VPS Disk Fills Mid‑Night: Quick Recovery with LVM Snapshots and Automated Alerts
- Rootless Podman volumes: how to keep data safe across upgrades without root