Nginx reverse proxy with SSL: Step-by-step setup for proxy_pass configuration, Let's Encrypt certificates, HTTPS redirects, security headers, WebSocket proxying, and Cloudflare integration.
An Nginx reverse proxy with SSL is how you expose backend applications to the internet securely. Instead of your Node.js API, Python app, or Docker container listening on a public port, Nginx sits in front — terminating TLS, handling HTTPS redirects, adding security headers, and forwarding requests to your application over a private connection. This guide covers the full setup from installing Nginx and obtaining Let's Encrypt certificates to production-ready configurations with security headers, WebSocket support, and Cloudflare origin SSL.
Why Use Nginx as a Reverse Proxy
Most application frameworks (Express, Django, Flask, Rails) include a built-in HTTP server. These are designed for development, not production. They handle one request at a time or have basic concurrency — Nginx handles tens of thousands of concurrent connections with minimal memory. Beyond performance, Nginx gives you:
- SSL termination — your app doesn't need to handle certificates
- HTTP to HTTPS redirect — all traffic encrypted by default
- Security headers — HSTS, X-Frame-Options, CSP in one place
- Static file serving — offload static assets from your application
- Rate limiting — protect against abuse before traffic reaches your app
- Multiple backends — serve different apps on different paths or subdomains
I run Nginx in front of every web application I deploy, whether it's running directly on the host or inside Docker Compose. The configuration patterns are the same either way.
Installing Nginx on Ubuntu and Debian
Install from the default repositories and enable auto-start:
sudo apt update && sudo apt install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginx
Verify it's running by visiting your server's IP in a browser — you should see the default Nginx welcome page. If you have UFW enabled, allow HTTP and HTTPS:
sudo ufw allow 'Nginx Full'
Nginx configuration lives in /etc/nginx/. The main file is nginx.conf, and site-specific configs go in /etc/nginx/sites-available/ with symlinks in /etc/nginx/sites-enabled/.
Basic Reverse Proxy Configuration with proxy_pass
How do I configure Nginx to proxy requests to a backend app?
Create a new site configuration. The proxy_pass directive tells Nginx where to forward requests. If your API runs on localhost:3000:
# /etc/nginx/sites-available/myapp.conf
server {
listen 80;
server_name example.com www.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 site and test the configuration:
sudo ln -s /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
The proxy_set_header lines are critical. Without X-Real-IP, your application sees all requests coming from 127.0.0.1 instead of the actual client IP. Without X-Forwarded-Proto, your app can't tell if the original request was HTTP or HTTPS — which breaks redirect logic and secure cookie flags. Always test with nginx -t before reloading.
SSL with Let's Encrypt and Certbot
How do I get a free SSL certificate for my Nginx server?
Let's Encrypt provides free, automatically renewable SSL certificates. Certbot is the official client that handles everything — generating the certificate, configuring Nginx, and setting up auto-renewal:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
Certbot modifies your Nginx config automatically: it adds the ssl_certificate and ssl_certificate_key directives, creates an HTTP-to-HTTPS redirect, and configures modern TLS settings. After running, your site is accessible over HTTPS with a valid certificate.
Verify auto-renewal is set up:
sudo certbot renew --dry-run
Certbot installs a systemd timer (or cron job) that renews certificates before they expire (every 90 days, with renewal attempts at 60 days). If the dry run succeeds, you never need to think about certificate renewal again.
Production Nginx Configuration with SSL and Security Headers
Certbot's auto-generated config is a good start, but a production setup needs security headers, optimized SSL settings, and proper caching. Here's the complete configuration I use:
# /etc/nginx/sites-available/myapp.conf
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# SSL certificates (managed by Certbot)
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Modern TLS configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Reverse proxy to application
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;
proxy_buffering on;
proxy_buffer_size 16k;
proxy_buffers 4 16k;
}
# Static files (served directly by Nginx)
location /static/ {
alias /var/www/myapp/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
}
What do the security headers protect against?
Strict-Transport-Security (HSTS) tells browsers to always use HTTPS for this domain — even if a user types http://. The 2-year max-age with includeSubDomains covers your entire domain. X-Frame-Options SAMEORIGIN prevents your site from being embedded in iframes on other domains (clickjacking protection). X-Content-Type-Options nosniff stops browsers from guessing MIME types — a vector for XSS attacks through uploaded files. Referrer-Policy controls how much URL information is sent when users click links to other sites.
Test with nginx -t and reload:
sudo nginx -t && sudo systemctl reload nginx
Proxying Multiple Applications on Different Paths
How do I serve multiple backends from one Nginx server?
Use separate location blocks to route traffic to different backend services:
server {
listen 443 ssl http2;
server_name example.com;
# ... SSL and headers as above ...
# Frontend app
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;
}
# API backend
location /api/ {
proxy_pass http://127.0.0.1:4000/;
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;
}
# Admin panel (IP-restricted)
location /admin/ {
allow 203.0.113.50;
deny all;
proxy_pass http://127.0.0.1:5000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Note the trailing slash on proxy_pass http://127.0.0.1:4000/. With the slash, Nginx strips the /api/ prefix before forwarding — so /api/users hits the backend as /users. Without the slash, the full path is forwarded. This is one of the most common sources of 404 errors in reverse proxy setups.
The admin panel uses allow/deny directives for IP-based access control — an additional layer on top of UFW firewall rules.
WebSocket Proxying with Nginx
How do I proxy WebSocket connections through Nginx?
WebSocket connections start as an HTTP upgrade request. Nginx needs two additional headers to pass the upgrade through to your backend:
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_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
The Upgrade and Connection headers are what make WebSocket proxying work. Without them, the connection stays as regular HTTP and the WebSocket handshake fails. proxy_read_timeout 86400s (24 hours) prevents Nginx from closing idle WebSocket connections — the default 60-second timeout kills long-lived connections that are waiting for events.
Cloudflare Origin SSL Setup
Do I still need SSL on my server if I use Cloudflare?
Yes. Without origin SSL, the connection between Cloudflare and your server is unencrypted — anyone intercepting traffic between Cloudflare's edge and your server can read it. Cloudflare offers free origin certificates specifically for this:
- Go to Cloudflare Dashboard → SSL/TLS → Origin Server
- Create a certificate (valid up to 15 years)
- Download the certificate and private key
- Place them on your server (e.g.
/etc/nginx/ssl/origin.crtand/etc/nginx/ssl/origin.key)
ssl_certificate /etc/nginx/ssl/origin.crt;
ssl_certificate_key /etc/nginx/ssl/origin.key;
Set Cloudflare's SSL mode to Full (Strict) — this ensures Cloudflare validates your origin certificate. "Full" without "Strict" accepts self-signed certificates, which defeats the purpose. With this setup, traffic is encrypted from user to Cloudflare (their edge certificate) and from Cloudflare to your server (your origin certificate). I use Cloudflare origin certs on most of my production servers because they never expire (practically) and don't require renewal automation.
Nginx Rate Limiting for API Protection
Protect your backend from abuse and DDoS with Nginx's built-in rate limiting:
# In the http block of /etc/nginx/nginx.conf
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
# In your server block
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://127.0.0.1:4000/;
# ... proxy headers ...
}
location /api/auth/login {
limit_req zone=login burst=5;
proxy_pass http://127.0.0.1:4000/auth/login;
# ... proxy headers ...
}
The api zone allows 10 requests per second with a burst of 20 — covering normal usage with room for page loads that trigger multiple API calls. The login zone limits to 1 request per second — aggressive, but login endpoints are the primary target for credential stuffing. nodelay processes burst requests immediately rather than spacing them out; omit it for login to enforce actual throttling.
Troubleshooting Common Nginx Reverse Proxy Issues
- 502 Bad Gateway — your backend isn't running or Nginx can't reach it. Check with
curl http://127.0.0.1:3000from the server - 504 Gateway Timeout — the backend is too slow. Increase
proxy_read_timeoutor investigate the slow endpoint - Real IP shows 127.0.0.1 — missing
proxy_set_header X-Real-IP. Your app needs to read fromX-Real-IPorX-Forwarded-For - Redirect loop — often caused by the app forcing HTTPS while Nginx already handles it. Set
X-Forwarded-Protoand configure your app to trust the proxy - Large request body rejected — Nginx defaults to 1 MB. Add
client_max_body_size 50m;for file uploads
Always check the Nginx error log for details:
sudo tail -50 /var/log/nginx/error.log
For the complete server security stack — SSH, firewall, and kernel hardening alongside your Nginx proxy — see the Linux server hardening checklist and the VPS security guide. For a deeper dive into Nginx configuration, check the Nginx wiki reference.