I have been running nginx as a reverse proxy for years and I still see people overcomplicating it. SSL termination, upstream routing, rate limiting ( it is all straightforward once you stop cargo-culting random blog posts from 2017. Here is how I actually set it up.
Why a Reverse Proxy at All
One server, multiple services. That is the whole point. I run Ghost, a few Python APIs, and sometimes a static site ( all on the same box. Without nginx in front, I would need a port for everything and the URL bar would look like garbage.
nginx handles the SSL certificate, routes traffic by hostname, and deals with the messy parts so my apps do not have to.
The Config I Actually Use
Here is the nginx config for this very blog. One file, minimal, done.
# /etc/nginx/sites-available/davideandreazzini.co.uk
server {
listen 80;
server_name davideandreazzini.co.uk www.davideandreazzini.co.uk;
return 301 https://davideandreazzini.co.uk$request_uri;
}
server {
listen 443 ssl http2;
server_name davideandreazzini.co.uk;
ssl_certificate /etc/letsencrypt/live/davideandreazzini.co.uk/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/davideandreazzini.co.uk/privkey.pem;
# Modern TLS only
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://127.0.0.1:2368;
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;
}
}That is it. No 200-line config file. No custom log formats nobody reads. Two server blocks: one redirects HTTP to HTTPS, one does the actual proxying.
SSL with Certbot ( Do Not Overthink It )
I covered SSL and Let us Encrypt in a previous post so I will not repeat myself. The short version:
sudo certbot --nginx -d davideandreazzini.co.uk -d www.davideandreazzini.co.ukCertbot writes the SSL lines for you. Renewal is automatic. Move on.
Adding More Services Behind the Same nginx
One config file per domain or subdomain. That is the pattern I stick to.
# /etc/nginx/sites-available/api.davideandreazzini.co.uk
server {
listen 443 ssl http2;
server_name api.davideandreazzini.co.uk;
ssl_certificate /etc/letsencrypt/live/api.davideandreazzini.co.uk/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.davideandreazzini.co.uk/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8000;
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;
}
}Copy the same structure, change the port. Enable with ln -s /etc/nginx/sites-available/api.davideandreazzini.co.uk /etc/nginx/sites-enabled/. Reload nginx. Done.
Rate Limiting ( Add It Later, Not First )
People throw rate limiting into their config on day one and then wonder why their API calls fail intermittently. Do not do that. Get the proxy working first, then add rate limiting when you actually need it.
# In http block ( /etc/nginx/nginx.conf )
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
# In your server block
location /api/ {
limit_req zone=general burst=20 nodelay;
proxy_pass http://127.0.0.1:8000;
# ... headers
}10 requests per second per IP, burst of 20. That is enough for normal traffic. If you are running a public API that gets hammered, adjust accordingly ( but start conservative and relax later, not the other way around ).
WebSockets ( One Extra Block )
If you are running anything with WebSockets behind nginx, you need three extra lines. That is it.
location /ws/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}Do not forget the proxy_http_version 1.1 line. Without it, nginx defaults to HTTP/1.0 and your WebSocket connections will silently fail in the most confusing way possible.
Mistakes I Have Made So You Do Not Have To
Forgetting the X-Forwarded-Proto header. Your app thinks it is running on HTTP when it is actually HTTPS. Redirect loops are fun.
Not testing the config before reloading. nginx -t takes two seconds. Use it every time.
Putting everything in one massive config file. One file per domain. It is not hard.
Not enabling the sites-enabled symlink. I have debugged why my config is not loading for an embarrassing amount of time before realizing I forgot the symlink.
Conclusion
nginx reverse proxy is not complicated. The minimal config above handles 95 percent of use cases. Stop copying 300-line configs from Stack Overflow and start with something you actually understand.
Add complexity when you need it, not before.