I have been running nginx as a reverse proxy for years. Every new server, every new project, same setup. It took me way too long to stop overcomplicating it. Here is the exact configuration I use now, stripped down to what actually matters.
The Minimal Config
Every reverse proxy needs three things: an upstream, a server block, and the proxy pass. That is it. Everything else is decoration you add later when you need it.
Here is the bare minimum:
upstream myapp {
server 127.0.0.1:3000;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://myapp;
}
}Save that to /etc/nginx/sites-available/myapp, symlink it to sites-enabled, run nginx -t and systemctl reload nginx. Done. Ten minutes tops if you type slow.
Headers That Actually Matter
Out of the box, nginx passes the request through but strips some headers your backend needs. The three I always add:
location / {
proxy_pass http://myapp;
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;
}Without these, your app thinks every request comes from 127.0.0.1 ( which is nginx, not the real client ), the host header is wrong, and HTTPS detection breaks. I have lost entire afternoons to a missing X-Forwarded-Proto header. Do not be me.
WebSocket Support ( Because Everything Uses WebSockets Now )
If you are running anything real-time ( chat, notifications, live data ), you need WebSocket upgrade support. Nginx drops WebSocket connections by default. Add this inside your location block:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";That is two lines and a version bump. If your WebSocket keeps disconnecting every 60 seconds, this is why.
SSL with Let's Encrypt
I covered the full SSL setup in a previous post, but for the reverse proxy specifically, here is the server block that listens on 443:
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privstkey.pem;
location / {
proxy_pass http://myapp;
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_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}And redirect HTTP to HTTPS:
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}Running Multiple Backends on One Server
This is where nginx actually earns its keep. I run four services on one VPS. Each gets its own server_name and upstream:
upstream app1 {
server 127.0.0.1:3000;
}
upstream app2 {
server 127.0.0.1:4000;
}
# app1.example.com -> port 3000
# app2.example.com -> port 4000
# One nginx, zero drama.No Docker networking nightmares, no port conflicts. Nginx handles the routing and your apps just listen on different local ports.
The Mistakes I Made ( So You Do Not Have To )
1. Forgetting proxy_set_header Host $host. Your app receives the upstream name instead of the actual domain. Routes break, cookies misbehave, sessions vanish.
2. Not setting X-Forwarded-Proto. Your app has no idea the client is using HTTPS. Redirect loops. Login forms that submit over HTTP. Bad times.
3. Using proxy_pass http://127.0.0.1:3000 directly instead of an upstream block. Works fine until you need load balancing or want to swap backends without editing five config files.
The Config I Actually Ship
I keep a template on my dotfiles repo. Every new VPS gets the same 30-line config file. SSL, headers, WebSockets, redirect. It just works.
Stop reading blog posts with 200-line nginx configs ( including this one, I guess ). Copy the minimal version. Add only what you need. Ship it.