Skip to content

Nginx reverse proxy and SSL — the complete guide

8 min read

What a reverse proxy is (and why you want one)

A reverse proxy is a web server (like Nginx) that sits in front of your application and handles incoming HTTP/HTTPS traffic on its behalf. Instead of clients connecting directly to your Node.js/Python/PHP app, they connect to Nginx, and Nginx forwards requests to your app.

Common benefits:

  • TLS/SSL termination: Nginx handles HTTPS certificates; your app can run on plain HTTP internally.
  • One public entrypoint: Host multiple apps/sites on one server via virtual hosts.
  • Performance: Nginx is very good at serving static files, caching, and handling slow clients.
  • Security & control: Add rate limiting, security headers, access control, and request size limits at the edge.
  • Zero-downtime deploys: Restart your app without dropping client connections as aggressively.
In this guide, you’ll set up Nginx as a reverse proxy, add SSL with Certbot, enable HTTP/2, add security headers, configure rate limiting, and cover common app backends.

---

Installing Nginx and understanding server blocks

On Ubuntu/Debian:

```bash sudo apt update sudo apt install nginx ```

Nginx config is typically in:

  • `/etc/nginx/nginx.conf` (main config)
  • `/etc/nginx/sites-available/` and `/etc/nginx/sites-enabled/` (site configs)
A typical workflow:

1. Create a config in `sites-available` 2. Symlink it into `sites-enabled` 3. Test and reload Nginx

```bash sudo nginx -t sudo systemctl reload nginx ```

A server block is like a “virtual host”—it matches a domain/IP and defines how to handle requests.

---

Reverse proxy basics: `proxy_pass` and essential headers

At the core of reverse proxying is `proxy_pass`, which forwards requests to an upstream server (your app).

Example: proxy `https://example.com` to a Node.js app running locally on port 3000.

Create `/etc/nginx/sites-available/example.com`:

```nginx server { listen 80; server_name example.com www.example.com;

location / { proxy_pass http://127.0.0.1:3000;

# Preserve client and request information 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 it:

```bash sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx ```

Why those headers matter

  • `Host`: ensures your app sees the original domain (useful for routing, generating URLs).
  • `X-Real-IP` and `X-Forwarded-For`: preserves the real client IP (otherwise your app sees `127.0.0.1`).
  • `X-Forwarded-Proto`: tells your app whether the original connection was HTTP or HTTPS (important after you enable SSL).
Many web frameworks can be configured to trust proxy headers. For example:
  • Express: `app.set('trust proxy', 1)`
  • Django: `SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')` (used carefully)
  • Flask (behind proxy): use `ProxyFix` middleware

WebSockets support (common for Node.js)

If your app uses WebSockets (Socket.IO, ws), add:

```nginx proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; ```

---

Adding SSL/TLS with Certbot (Let’s Encrypt)

Let’s Encrypt provides free certificates. Certbot automates issuing and renewing them.

Install Certbot

On Ubuntu/Debian:

```bash sudo apt install certbot python3-certbot-nginx ```

Obtain and install a certificate

Make sure your DNS A/AAAA records point to this server, then run:

```bash sudo certbot --nginx -d example.com -d www.example.com ```

Certbot will:

  • Validate domain ownership (usually via HTTP challenge)
  • Modify your Nginx config to add `listen 443 ssl;`
  • Install certificate paths
  • Optionally configure HTTP → HTTPS redirect

Verify auto-renewal

Let’s Encrypt certs are short-lived; renewal is automatic via systemd timers/cron.

Test renewal:

```bash sudo certbot renew --dry-run ```

---

Enabling HTTP/2

HTTP/2 improves performance by multiplexing requests over a single connection (especially noticeable for sites with many assets).

In modern Nginx, enable it on your TLS listener:

```nginx 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/privkey.pem;

# ... } ```

Certbot often adds this automatically; if not, you can add `http2` yourself and reload Nginx.

---

Recommended SSL settings (practical and safe defaults)

You can keep SSL config inside the server block or include a snippet. A simple modern baseline:

```nginx ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off;

Session settings (optional)

ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ```

Nginx defaults are generally reasonable in recent versions, but explicitly setting TLS 1.2/1.3 helps avoid legacy protocols.

---

Security headers (and what they do)

Security headers reduce risk from common web attacks. Add them in your HTTPS server block. A practical set:

```nginx add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

HSTS: forces HTTPS for future visits (enable only after HTTPS works reliably)

add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always; ```

About Content-Security-Policy (CSP)

CSP is powerful but can break sites if misconfigured. Start with a report-only policy or a minimal one you expand carefully:

```nginx

Example only; adjust to your assets/CDNs

add_header Content-Security-Policy "default-src 'self'" always; ```

Notes and common pitfalls

  • Only enable HSTS after you’re sure you won’t serve HTTP again; otherwise you can lock yourself out.
  • Use `always` so headers apply to error responses too.
---

Rate limiting (basic DDoS/abuse protection)

Nginx can limit request rates per IP. This helps with brute-force attempts and abusive clients.

Place these in the `http {}` block (usually in `/etc/nginx/nginx.conf`), or in a file included from it:

```nginx limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s; ```

Then apply in a location:

```nginx location /api/ { limit_req zone=api_limit burst=20 nodelay; proxy_pass http://127.0.0.1:3000; } ```

Meaning:

  • Allow ~10 requests/second per IP
  • Allow short bursts of 20
  • `nodelay` processes bursts immediately (without slowing), but still caps sustained rate
You can also limit connections:

```nginx limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

server { # ... limit_conn conn_limit 20; } ```

---

Common reverse proxy configurations

Node.js (Express / Next.js / NestJS)

A strong baseline for Node apps on `127.0.0.1:3000`:

```nginx server { listen 80; server_name example.com www.example.com;

location /.well-known/acme-challenge/ { root /var/www/html; } location / { return 301 https://$host$request_uri; } }

server { listen 443 ssl http2; server_name example.com www.example.com;

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "DENY" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;

client_max_body_size 20m;

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;

# WebSockets support proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } } ```

Practical tip: run Node behind a process manager like systemd or PM2 so it restarts on crash/reboot.

Python (Gunicorn + Flask/Django)

Most Python production setups run behind Gunicorn (HTTP) or uWSGI. Here’s Gunicorn on `127.0.0.1:8000`:

```nginx 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; } ```

Static files (recommended): let Nginx serve them directly:

```nginx location /static/ { alias /var/www/myapp/static/; expires 30d; add_header Cache-Control "public"; } ```

For Django, remember to configure `ALLOWED_HOSTS`, and if using proxy headers, do it intentionally and securely.

PHP (PHP-FPM) — not a reverse proxy, but common with Nginx

PHP is typically served via FastCGI, not `proxy_pass`. Example:

```nginx server { listen 443 ssl http2; server_name example.com; root /var/www/example.com/public; index index.php index.html;

ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

location / { try_files $uri $uri/ /index.php?$query_string; }

location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.2-fpm.sock; }

location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg)$ { expires 30d; access_log off; } } ```

If you’re hosting both PHP and a Node API, Nginx can route by path:

  • `/` → PHP site
  • `/api/` → Node upstream via `proxy_pass`
---

Troubleshooting and validation

Test configuration

Always run:

```bash sudo nginx -t sudo systemctl reload nginx ```

Confirm your site is using HTTPS and HTTP/2

Use curl:

```bash curl -I https://example.com ```

To verify HTTP/2:

```bash curl -I --http2 https://example.com ```

Common issues

  • 502 Bad Gateway: your upstream app is down, wrong port, or blocked by firewall/permissions.
  • Infinite redirect loop: app doesn’t trust `X-Forwarded-Proto` and keeps redirecting to HTTPS (or back to HTTP). Ensure proxy headers are set and the app is configured to trust them.
  • Large uploads failing (413): increase `client_max_body_size`.
  • Wrong client IP in logs: ensure `X-Forwarded-For` is set; configure your app/framework to use it.
---

A practical “good default” checklist

  • Nginx server block per domain (`server_name`)
  • `proxy_pass` to your app on localhost/private network
  • Set `Host`, `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`
  • Certbot-managed TLS certs + renewal tested
  • HTTP → HTTPS redirect
  • `listen 443 ssl http2;`
  • Basic security headers (plus HSTS once stable)
  • Rate limit sensitive endpoints (`/login`, `/api`)
  • Serve static files directly where possible
With these pieces, you’ll have a production-ready Nginx reverse proxy setup that’s secure, maintainable, and flexible enough to host multiple applications behind one IP.