nginx reverse proxy with TLS (2026)

2026-06-28 · 5 min read

SHORT ANSWER Run two server blocks: one on port 80 that 301-redirects everything to HTTPS, and one on port 443 that terminates TLS and proxy_passes to your app's upstream. Forward Host and the X-Forwarded-* headers so the app sees the real client, and restrict TLS to 1.2 and 1.3. Copy-paste config below.

The config

upstream app {
    # Your app — same host, container, or LAN. Keep it on localhost/private net.
    server 127.0.0.1:8000;
}

# HTTP → HTTPS: redirect everything
server {
    listen 80;
    listen [::]:80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

# HTTPS: terminate TLS, proxy to the app
server {
    listen 443 ssl;
    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;

    # Modern TLS only
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;

    location / {
        proxy_pass http://app;
        proxy_http_version 1.1;

        # Pass the real client + scheme to the app
        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 X-Forwarded-Host  $host;

        # WebSocket / connection upgrade support
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_read_timeout 60s;
    }
}

Why each part matters

Two server blocks

Port 80 exists only to bounce plain HTTP up to HTTPS with a 301 — permanent, so browsers and search engines remember it. All real traffic lands on the port-443 block, which is the only place TLS is configured.

Forwarded headers

Behind a proxy your app no longer sees the client directly — it sees nginx. Host, X-Real-IP, X-Forwarded-For and X-Forwarded-Proto hand the real hostname, client IP, and original scheme through. Without X-Forwarded-Proto the app thinks every request is plain HTTP and may build broken http:// redirects or refuse secure cookies.

Modern TLS only

ssl_protocols TLSv1.2 TLSv1.3 drops SSLv3, TLS 1.0 and 1.1 — all deprecated and failing PCI/security scans. With TLS 1.3 in the mix you let the client pick the cipher (ssl_prefer_server_ciphers off), which is the current recommendation.

Connection upgrade

The Upgrade / Connection headers plus proxy_http_version 1.1 let WebSockets pass through. Omit them and your real-time connections silently fail to handshake.

Getting the certificate

The config above points at Let's Encrypt paths. Issue and auto-renew the cert with certbot:

sudo certbot --nginx -d example.com
# certbot installs a systemd timer that renews automatically

For containers, mount the certs in or use a companion ACME container — the nginx config itself is identical; only the certificate paths change.

Common mistakes

Generate this for your app → SysBuild's free nginx generator builds reverse-proxy + TLS server blocks for your hostname and upstream — in your browser, no signup.

Going further

For a complete, ready-to-run project — app, Dockerfile, docker-compose with Postgres, CI, and a health endpoint already wired — the SysBuild Pro Pack ships six production stacks (including fastapi-postgres and node-express-postgres, the exact apps you'd put behind this proxy) for $29 one-time.

More articles