Setting up an nginx reverse proxy is the most practical way to put a solid gate in front of your application server instead of exposing it directly to the internet. Whether it is a Node.js API, a PHP-FPM process serving Laravel, or a game panel UI listening on port 8080, Nginx receives the incoming requests, terminates TLS, serves static files and forwards the rest to the app behind it. In this article we build a working configuration from scratch, cover the common pitfalls and add the fine-tuning that production needs, step by step.
What exactly does a reverse proxy do?
A classic proxy sits on the client side and manages outgoing traffic. A reverse proxy is the opposite: it sits on the server side and handles incoming traffic. The client only ever sees Nginx; it never knows which port, language or how many copies the backend app is running on. This abstraction buys you a lot:
- TLS termination: Certificate management is centralized at a single point, in Nginx. The app speaks plain HTTP.
- One port, one domain: You accept everything on 80 and 443 and route different paths (
/api,/admin) to different backends. - Security: The app port stays closed to the outside; it is reachable only from localhost.
- Performance: Static files, gzip/brotli compression and caching are handled directly by Nginx.
Installation and basic configuration
On Debian/Ubuntu the install is a one-liner:
sudo apt update
sudo apt install nginx
sudo systemctl enable --now nginx
The cleanest approach is to keep configuration files under /etc/nginx/sites-available/ and enable them with a symlink into sites-enabled/. Say your app runs on 127.0.0.1:3000. The server block below forwards all incoming requests to it:
server {
listen 80;
server_name app.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;
}
}
Enable the file, test the configuration and reload:
sudo ln -s /etc/nginx/sites-available/app \
/etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Why are the proxy headers so important?
The proxy_set_header lines above are not decoration. The moment Nginx steps in, the backend app can no longer see the real client; all it faces is 127.0.0.1. If you do not pass the right headers, everyone's IP shows up as localhost in your logs, your app mistakes HTTPS for HTTP and falls into an infinite redirect loop, or session cookies break.
X-Forwarded-Forcarries the real client IP.X-Forwarded-Prototells the app the request was actually HTTPS; secure cookies and correct URL generation depend on it.- Preserving the
Hostheader lets the app see the right host in multi-domain setups.
In frameworks like Laravel, remember to configure the trusted proxy setting so these headers are honored; otherwise the X-Forwarded-* headers are ignored.
HTTPS and TLS termination
The biggest strength of a reverse proxy is centralizing the certificate in one place. Certbot is the most practical tool for getting a free Let's Encrypt certificate:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com
Certbot automatically adds the 443 block, the certificate paths and the 80-to-443 redirect. If you prefer to write it manually, the skeleton looks like this:
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Because you terminate TLS at Nginx, the link to the backend can be plain HTTP; over localhost this is perfectly safe and frees the app from dealing with certificates at all.
WebSocket, timeouts and caching
Real-time apps (chat, live dashboards, a game console) use WebSocket. For Nginx to upgrade the connection, two headers are required:
location /ws {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 3600s;
}
For long-running requests, the default 60-second proxy_read_timeout can fool you; a heavy report generation gets cut off halfway. For static content, using Nginx's own cache or the expires directive answers the request without ever reaching the backend, taking load off the app.
Load balancing and multiple backends
When a single app instance is not enough, you define several processes as one pool with an upstream block:
upstream app_pool {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
}
server {
listen 80;
location / {
proxy_pass http://app_pool;
}
}
By default Nginx distributes requests in turn (round-robin); with least_conn it picks the one with the fewest connections, and with ip_hash it always sends the same client to the same backend. This is also the foundation for zero-downtime (blue-green) deployments.
Frequently Asked Questions
Are a reverse proxy and a load balancer the same thing?
Not exactly. Every load balancer behaves like a reverse proxy, but not every reverse proxy has to balance load. Nginx can do both: with a single backend it is just a proxy, and with multiple backends and an upstream it becomes a load balancer.
How do I fix a 502 Bad Gateway error?
A 502 almost always means the backend is not running or the address/port in proxy_pass is wrong. Confirm the app is actually listening with ss -ltnp and check /var/log/nginx/error.log.
Should I terminate TLS at the backend?
In most setups, no. Terminating TLS at Nginx and speaking plain HTTP over localhost is both simpler and secure. Only in strict compliance scenarios where internal traffic on the wire must also be encrypted do you consider end-to-end TLS.
Want to put your infrastructure behind Nginx? We can design a solid reverse proxy setup together, including TLS, caching and zero-downtime deployments. Get in touch with me and let's talk about your project.