Reverse proxy and TLS
Put an application behind nginx, forward the headers it needs, and terminate HTTPS in one place.
Proxying an application
location / {
proxy_pass http://127.0.0.1:3000; # no trailing slash: URI passes through
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade"; # websockets
proxy_read_timeout 60s;
}Without the forwarded headers the application sees nginx as the client, believes every request arrived over plain HTTP, and generates wrong absolute URLs or redirect loops.
⚠️
The trailing slash on
proxy_pass changes the path. proxy_pass http://app:3000/; strips the matched location prefix, while proxy_pass http://app:3000; forwards the URI unchanged. That one character causes more 404s than anything else in nginx.Upstream pools
upstream app {
least_conn; # or round-robin, or ip_hash
server 10.0.0.11:3000 max_fails=3 fail_timeout=15s;
server 10.0.0.12:3000 max_fails=3 fail_timeout=15s;
keepalive 32; # reuse upstream connections
}
location /api/ {
proxy_pass http://app;
proxy_next_upstream error timeout http_502 http_503;
}| Strategy | Behaviour |
|---|---|
| round-robin (default) | One request per server in turn |
least_conn | Send to the server with the fewest active connections |
ip_hash | Same client IP to the same server; useful for legacy session state |
weight=n | Bias traffic towards a stronger node |
A failed server is taken out of rotation for fail_timeout after max_fails errors, then tried again. proxy_next_upstream decides which errors are worth retrying on another node.
TLS termination
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
add_header Strict-Transport-Security "max-age=31536000" always;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header X-Forwarded-Proto $scheme; # tell the app it is HTTPS
}
}sudo certbot --nginx -d example.com -d www.example.com
sudo certbot renew --dry-run # certificates last 90 days; automate renewal- Terminating TLS here means one place to manage certificates, while internal traffic on a private network stays plain HTTP.
- Set
X-Forwarded-Protoso the application knows the original scheme; otherwise it mixes HTTPS pages with HTTP asset links. - Add the HSTS header only once you are confident HTTPS works everywhere, because browsers will refuse plain HTTP for the stated period.
FAQ
Why does my app redirect in an infinite loop?
It is forcing HTTPS while
X-Forwarded-Proto still says http, so it redirects again. Set the header in nginx and mark the proxy as trusted in the application's configuration.How do I renew certificates?
certbot renew is idempotent. Schedule it twice a day (systemd timer or cron), reload nginx afterwards, and always try --dry-run first.Related
Server blocks and static files Caching, compression and traps
Last refreshed 2026-09-18.