The first time someone told me "just put a reverse proxy in front of it", I nodded like I understood and then went home and read documentation for three hours. The concept sounded like something you needed a networking degree for. It is not. A reverse proxy is a receptionist for your server: one desk, one public face, and it knows which office to send each visitor to.
Once that clicks, nginx reverse proxy configs stop looking like magic and start looking like what they are — a few lines of directions.
The problem it solves
Say your little server runs three apps:
- Nextcloud on
127.0.0.1:8080 - a small API on
127.0.0.1:3000 - a dashboard on
127.0.0.1:9000
Without a proxy, the world can only reach them as http://your-ip:8080, :3000, :9000 — ugly URLs, every port open in the firewall, and each app responsible for its own HTTPS.
With a reverse proxy, you get:
-
https://cloud.example.com→ Nextcloud -
https://api.example.com→ the API -
https://dash.example.com→ the dashboard
One public port (443), one place where certificates live, apps bound to localhost where the internet cannot touch them directly. That last part is the security win people underrate: the only thing exposed to the internet is nginx, which is very good at being exposed to the internet.
The smallest real config
Here is a complete reverse proxy for one app, nothing decorative:
server {
listen 80;
server_name cloud.example.com;
location / {
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;
}
}
Line by line, in plain English:
-
listen 80— answer HTTP (we will add HTTPS in a moment) -
server_name— which hostname this block answers for. This is how one nginx routes three domains to three apps -
proxy_pass— where the app actually lives - The four
proxy_set_headerlines — pass along who the visitor really is. Without them, your app sees every request as coming from127.0.0.1(nginx itself), which breaks logging, rate limiting, and any "what is my IP" feature
Test and reload:
sudo nginx -t && sudo systemctl reload nginx
nginx -t before every reload. It catches typos in two seconds instead of at 2 AM.
DNS is the other half (and the half people forget)
The config above does nothing until cloud.example.com actually points at your server. Create an A record (or a CNAME to your main domain) for each subdomain:
cloud.example.com A YOUR_SERVER_IP
api.example.com A YOUR_SERVER_IP
dash.example.com A YOUR_SERVER_IP
All three point to the same IP. nginx sorts out which app each one wants using the Host header — that is the whole trick. If DNS is fresh, give it time to propagate and check with dig +short cloud.example.com from your laptop.
Add HTTPS the easy way
Do not hand-write TLS config. Use Certbot, which edits this exact file for you:
sudo certbot --nginx -d cloud.example.com -d api.example.com -d dash.example.com
It fetches the certificates, adds the listen 443 ssl lines, and redirects HTTP to HTTPS. The full walkthrough including the renewal dry-run is in our Let's Encrypt guide — do that dry-run, future you will be grateful.
The three problems everyone hits
1. "It works but my app shows all visitors as 127.0.0.1." You skipped the X-Forwarded-For headers, or the app does not trust the proxy. Most apps have a "trusted proxies" setting — add 127.0.0.1 there.
2. "502 Bad Gateway." nginx cannot reach the app. Check the app is actually running and listening where you said: ss -tlnp | grep 8080. Also check whether it is bound to 127.0.0.1 vs 0.0.0.0 — for a same-machine proxy, localhost binding is what you want, so this is usually just "the app is down".
3. "The site loads but login cookies / WebSockets break." Some apps need proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; for WebSockets (dashboards and chat apps are the usual suspects). Cookie issues are often the app generating URLs for the wrong domain — set its external URL/base URL setting to the public https:// address.
When to reach for something else
nginx is my default, and it is what runs in front of this site. But if you want automatic HTTPS with zero config, Caddy is genuinely lovely. If you want a GUI, Nginx Proxy Manager is popular for a reason. If you run lots of Docker containers with changing labels, Traefik auto-discovers them. The concepts above — one public face, hostname routing, forwarded headers — transfer to all of them, so learning them once pays off everywhere.
Start with one app and one subdomain. The moment https://cloud.example.com loads your app with a valid certificate and only port 443 open, you will wonder why this ever seemed complicated.
Top comments (0)