Nginx Reverse Proxy and TLS: A Production Setup Guide
Nginx Reverse Proxy and TLS: A Production Setup Guide
Nginx is the Swiss Army knife sitting in front of most production stacks: TLS termination, reverse proxying, static files, caching, rate limiting. This tutorial takes a fresh Nginx install to a hardened TLS reverse proxy.
Table of Contents
- Install and verify
- Reverse proxy: the core pattern
- Free TLS with Let's Encrypt
- Load balancing: upstreams
- Production hardening checklist
- Reading logs like a pro
- When nginx is the wrong tool
Install and verify
# Debian/Ubuntu
sudo apt update && sudo apt install nginx
sudo systemctl enable --now nginx
curl -I http://localhost # 200 OK
Config lives in /etc/nginx/nginx.conf with sites in sites-available/, enabled via symlinks into sites-enabled/.
Reverse proxy: the core pattern
Your app listens on 127.0.0.1:3000; Nginx faces the internet:
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
Those headers matter — your app sees the real client IP and original scheme, not nginx's. Without X-Forwarded-Proto, apps often break on redirects or mixed-content detection.
sudo nginx -t # always test before reload
sudo systemctl reload nginx
Free TLS with Let's Encrypt
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com
Certbot edits the config, adds the TLS block, and sets up auto-renewal. Verify with sudo certbot renew --dry-run. This turns your port-80 proxy into a proper HTTPS endpoint in about 30 seconds.
Load balancing: upstreams
upstream app_backend {
least_conn; # send to least-busy
server 10.0.1.11:3000;
server 10.0.1.12:3000;
server 10.0.1.13:3000 backup; # only if others fail
}
location / {
proxy_pass http://app_backend;
}
Production hardening checklist
# inside http {} or server {}
client_max_body_size 10m; # reject huge uploads
server_tokens off; # don't advertise version
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header Referrer-Policy strict-origin-when-cross-origin;
# rate limiting — protects login endpoints
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
location /api/login {
limit_req zone=login burst=10 nodelay;
proxy_pass http://app_backend;
}
Reading logs like a pro
tail -f /var/log/nginx/access.log
tail -f /var/log/nginx/error.log
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head
When nginx is the wrong tool
If you're on Kubernetes, use an Ingress controller (NGINX Ingress, Traefik). For pure L4 TCP at massive scale, look at HAProxy or a cloud NLB. But for VMs, containers on a single host, and everything in between — this config covers 90% of production needs.
Related Articles
- Essential Linux Commands Every DevOps Engineer Needs
- AWS for DevOps Engineers: The Services That Actually Matter
Last Updated: October 2026 Author: CloudOpsGuide Team Difficulty: Intermediate Estimated Reading Time: 10 minutes