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 301 is a permanent redirect; browsers and search engines remember it.
  • $request_uri keeps the path and query string: http://example.com/blog/?page=2 → https://example.com/blog/?page=2.
  • Prefer return over rewrite — 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 (WordPress siteurl) 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.

  • Nginx
  • SSL
  • SEO