Once a site has a TLS certificate, every visitor should end up on https://, at one canonical hostname. In Nginx that’s a few lines — but there are a couple of traps (redirect loops behind proxies, if-statements, broken certificate renewals) worth avoiding.
The standard redirect
Add a separate server block for port 80 that does nothing but redirect:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
return 301is a permanent redirect; browsers and search engines remember it.$request_urikeeps the path and query string:http://example.com/blog/?page=2→https://example.com/blog/?page=2.- Prefer
returnoverrewrite— it’s faster and clearer.
Your HTTPS server block handles the real site:
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;
root /var/www/example.com;
# …
}
Test and reload:
sudo nginx -t && sudo systemctl reload nginx
(Certbot’s --nginx plugin adds a similar redirect for you — see Let’s Encrypt with Certbot.)
Canonical hostname: www → non-www (or the reverse)
Pick one hostname and redirect the other — including on HTTPS. Otherwise https://www.example.com serves a duplicate of your site.
# HTTPS www → HTTPS non-www
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
return 301 https://example.com$request_uri;
}
The certificate must cover both names (-d example.com -d www.example.com), because the browser checks it before seeing the redirect.
With the port-80 block above, every combination ends up at https://example.com in one hop:
| Request | Result |
|---|---|
http://example.com |
301 → https://example.com |
http://www.example.com |
301 → https://example.com |
https://www.example.com |
301 → https://example.com |
https://example.com |
200 |
Prefer www? Swap the names. More in redirecting www to non-www.
Keep Let’s Encrypt HTTP-01 challenges working
Let’s Encrypt follows redirects to HTTPS during HTTP-01 validation, so a blanket redirect usually works. If you use certbot certonly --webroot and want challenges served over plain HTTP regardless, exempt the path:
server {
listen 80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
}
location / {
return 301 https://example.com$request_uri;
}
}
Avoid “if” for redirects
You’ll see this in older tutorials:
if ($scheme = http) {
return 301 https://$host$request_uri;
}
It works, but separate server blocks are clearer, cheaper and avoid the well-known pitfalls of if in Nginx. Also prefer a fixed hostname over $host in the target — $host reflects whatever the client sent, which with a catch-all block could redirect to arbitrary domains.
Behind a load balancer or Cloudflare
If TLS terminates before Nginx (a load balancer or a CDN), requests reach Nginx over HTTP even when the visitor used HTTPS. Redirecting every HTTP request then creates an infinite redirect loop. Redirect based on the header the proxy sets:
if ($http_x_forwarded_proto = "http") {
return 301 https://$host$request_uri;
}
With Cloudflare, the better fix is SSL mode Full (strict) with a certificate on your origin, so Cloudflare connects over HTTPS — then the normal redirect is fine. “Flexible” mode plus an origin redirect is the classic loop. See real IP and headers behind Cloudflare.
HSTS: make browsers skip HTTP entirely
After a redirect is in place, the Strict-Transport-Security header tells browsers to use HTTPS directly next time:
add_header Strict-Transport-Security "max-age=31536000" always;
Add it in the HTTPS server block only. Start with a short max-age if you’re unsure, and read HSTS explained before adding includeSubDomains or preload.
301, 302, 307 or 308?
- 301 — permanent; the standard for HTTP → HTTPS. Browsers may change POST to GET when following it.
- 308 — permanent and preserves the method and body. Useful for APIs that receive POSTs on HTTP.
- 302/307 — temporary; use while testing, then switch to permanent.
Details in 301 vs 302 redirects.
Test
curl -I http://example.com/test?x=1
curl -I https://www.example.com/
curl -sIL http://www.example.com | grep -iE '^(HTTP|location)'
You want a single 301 with the correct Location, followed by 200.
Troubleshooting
ERR_TOO_MANY_REDIRECTS— redirect loop: usually Cloudflare Flexible SSL, a load balancer, or an application (WordPresssiteurl) redirecting back to HTTP. See WordPress too many redirects.- Certificate warning on www — the certificate doesn’t include
www. - Redirect goes to the wrong site — a catch-all
server_name _with$host; use fixed names.
Our VPS plans include a dedicated IPv4 address, so you can run HTTPS for as many domains as you like with free Let’s Encrypt certificates.