Redirecting HTTP to HTTPS in Nginx: Best Practices

Set up a reliable HTTP to HTTPS redirect in Nginx using a dedicated port 80 block, avoid redirect loops, choose the right redirect code, and add HSTS when ready.

Redirecting HTTP to HTTPS in Nginx: Best Practices

Redirecting HTTP to HTTPS in Nginx ensures visitors use the encrypted version of your site, even if they type the old http:// URL or follow an outdated link. A clean redirect setup improves security, avoids duplicate URLs, and gives users a consistent entry point.

The best approach is usually simple: keep port 80 open only long enough to redirect traffic, and serve the real site on port 443 with a valid TLS certificate.

The details matter because redirects are easy to make almost right. A redirect that drops the path breaks bookmarks. A redirect that preserves the wrong hostname can create duplicate canonical URLs. A redirect behind a load balancer can loop forever if Nginx does not understand where TLS terminates.

Think of the redirect as part of your public API. People paste links into chat, search engines crawl them, monitoring systems hit them, and old emails keep them alive for years. If the redirect is stable, nobody notices. If it is sloppy, users see certificate warnings, broken paths, or too-many-redirects errors before your application even gets a request.

Why HTTPS Redirects Matter

HTTPS protects traffic between the browser and your server by using TLS encryption. Without it, data can be inspected or modified by networks between the user and your site. That matters for logins, forms, cookies, admin areas, APIs, and even ordinary browsing.

Redirects also help with consistency. Search engines and users should not see http://example.com/page and https://example.com/page as two separate destinations. A permanent redirect tells clients that HTTPS is the preferred version.

The standard Nginx pattern is a dedicated port 80 server block:

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

    return 301 https://$host$request_uri;
}

Then your HTTPS server block handles the actual site:

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;

    root /var/www/example.com;
    index index.html;
}

The return 301 directive is efficient and clear. It tells Nginx to send a permanent redirect without doing extra location processing. $request_uri preserves the path and query string, so /docs?page=2 becomes https://example.com/docs?page=2.

For a full TLS setup, see securing Nginx with HTTPS.

For most sites, avoid putting application logic in the port 80 block. It should not serve static files, proxy to the app, or contain a large set of locations. Keep it boring:

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://example.com$request_uri;
}

That small block is easier to audit and less likely to drift from the HTTPS configuration.

Choosing 301 vs 302 Redirects

Use 301 for a permanent HTTP-to-HTTPS redirect on a production site. Browsers and search engines understand that the HTTPS URL should replace the HTTP URL.

Use 302 or 307 only when the redirect is temporary. For example, you might use a temporary redirect during testing before certificates and hostnames are final. Once the HTTPS site is ready, switch to a permanent redirect.

Be careful during early setup. Browsers can cache 301 redirects aggressively. If you accidentally redirect to the wrong hostname, the browser may keep using the bad redirect even after you fix Nginx. Test with curl, private browser windows, and non-production hostnames when possible.

A practical deployment flow looks like this:

  1. Confirm the HTTPS server block works directly.
  2. Confirm the certificate matches every hostname.
  3. Add the HTTP redirect server block.
  4. Test several paths and query strings.
  5. Change temporary redirects to permanent redirects only when the behavior is correct.

You should also decide on the canonical hostname. If both example.com and www.example.com work, choose one as the preferred public hostname. Otherwise, users may bounce between hostnames or search engines may index both.

For example, to redirect all HTTP traffic to the non-www HTTPS hostname:

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

    return 301 https://example.com$request_uri;
}

That is different from https://$host$request_uri, which preserves whichever host the user requested.

You can also split hostname redirects from scheme redirects if you want the behavior to be explicit:

server {
    listen 80;
    server_name www.example.com;
    return 301 https://example.com$request_uri;
}

server {
    listen 80;
    server_name example.com;
    return 301 https://example.com$request_uri;
}

This is more verbose, but it makes the final destination obvious. On a small site, either style is fine. On a larger site with many aliases, explicit server blocks can reduce confusion during later changes.

Avoiding Redirect Loops and Certificate Problems

Redirect loops happen when Nginx, a load balancer, or an application keeps sending a request back to a URL that triggers another redirect. This is common when TLS terminates before Nginx, such as at a cloud load balancer or CDN.

In a simple single-server setup, Nginx receives HTTPS directly, so the redirect is straightforward. In a proxy chain, Nginx may receive plain HTTP from the load balancer even though the user connected with HTTPS. If your application then tries to force HTTPS based on the local connection scheme, it may cause a loop.

The fix depends on your architecture. Usually, the load balancer should pass headers such as X-Forwarded-Proto, and the application or Nginx config should trust them only from known proxy addresses.

For example, if Nginx sits behind a trusted load balancer and only receives internal HTTP, you may not want Nginx to redirect every local HTTP request. Instead, the load balancer can handle the public HTTP-to-HTTPS redirect, while Nginx serves traffic from the private network. If Nginx must make the decision, it needs reliable forwarded-protocol information from the proxy in front of it. Do not trust X-Forwarded-Proto from arbitrary internet clients.

Also make sure certificates cover every redirected hostname. If a user visits http://www.example.com and you redirect to https://www.example.com, the certificate must be valid for www.example.com. If you redirect everything to https://example.com, the certificate for the final site must cover example.com.

Test with:

curl -I http://example.com/some/path?x=1

Look for:

HTTP/1.1 301 Moved Permanently
Location: https://example.com/some/path?x=1

Then test the HTTPS URL:

curl -I https://example.com/some/path?x=1

The HTTPS response should return the actual status for the page, not another redirect back to HTTP.

Also test both hostnames if both exist:

curl -I http://www.example.com/
curl -I https://www.example.com/
curl -I http://example.com/
curl -I https://example.com/

You are looking for a short, predictable chain. One redirect from HTTP to the canonical HTTPS URL is good. Multiple hops, such as HTTP non-www to HTTPS non-www to HTTPS www and back again, are a sign that Nginx, the app, CDN rules, or DNS-level forwarding are fighting each other.

You can inspect the whole chain with:

curl -IL http://www.example.com/some/path

The -L flag follows redirects. In a clean setup, the output should show the initial HTTP response and then the final HTTPS response. If you see three or four Location headers, simplify the rules until there is one clear route to the canonical URL.

HSTS and Other Best Practices

After your HTTPS setup is stable, you can consider HTTP Strict Transport Security, usually called HSTS. HSTS tells browsers to use HTTPS automatically for future visits.

A common header looks like this:

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

Do not add this casually. If you include subdomains, every subdomain must support HTTPS. If you later break HTTPS, browsers that have seen the HSTS header may refuse to access the HTTP version. Start with a shorter max-age during testing, then increase it once you are confident.

Other best practices:

  • Keep the port 80 block simple.
  • Preserve paths and query strings unless you have a reason not to.
  • Pick one canonical hostname.
  • Test redirects with curl, not only a browser.
  • Renew certificates automatically and monitor renewal failures.
  • Keep redirect logic in Nginx when possible instead of duplicating it in the app.

Simple redirect rules are easier to reason about and less likely to break during later site changes.

Common Mistakes

The most common mistake is using rewrite for a simple redirect:

rewrite ^ https://example.com$request_uri permanent;

That can work, but return 301 ... is clearer and avoids extra rewrite processing. Use rewrite when you genuinely need pattern matching, not for a basic scheme redirect.

Another mistake is redirecting to $server_name without understanding what it contains. $host comes from the request host header, while $server_name is based on the matched Nginx server name. For canonical redirects, a literal hostname is often the least surprising option:

return 301 https://example.com$request_uri;

You should also avoid redirecting ACME HTTP challenge paths if your certificate tooling needs them on port 80. Many Let's Encrypt setups handle this automatically, but custom configs may need an exception:

location /.well-known/acme-challenge/ {
    root /var/www/letsencrypt;
}

location / {
    return 301 https://example.com$request_uri;
}

Only add that exception if your certificate client uses it. If your certificates renew through DNS validation or a tool-managed temporary server, keep the redirect block simple.

A Safe Rollout Pattern

For a production site, make the change in stages:

  1. Confirm the HTTPS server block serves the site correctly.
  2. Confirm certificate renewal works or is monitored.
  3. Add a temporary redirect on port 80 if you are still testing hostnames.
  4. Test common URLs with curl -I and curl -IL.
  5. Switch to 301 once the redirect target is final.
  6. Wait before enabling long-lived HSTS.

That waiting period is useful. It gives you time to catch forgotten subdomains, old webhook URLs, hardcoded http:// links, or a CDN rule that was not visible from your first test machine.

Also keep monitoring in mind. If your uptime check still points at http://example.com, decide whether it should expect a 301 or follow redirects and check the final HTTPS page. Both can be valid, but the monitor should match the behavior you actually want.

Example: Static Site and Reverse Proxy

For a static site, the HTTPS block may be only a document root:

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;

    root /var/www/example.com;
    index index.html;
}

For an application behind Nginx, the redirect block stays the same, but the HTTPS block proxies traffic:

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;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

The important point is separation. Port 80 decides where the browser should go. Port 443 serves the site. Mixing those jobs makes redirect behavior harder to reason about, especially when another proxy or CDN is added later.

After editing, always run:

sudo nginx -t
sudo systemctl reload nginx

If the test fails, do not reload. Fix the syntax error first, then test again.

One final check is worth doing from outside the server, not only over SSH on the host. A firewall, CDN, or load balancer can change what real users see. Run the same curl -I checks from your laptop, a monitoring location, or a temporary cloud instance. If the external result differs from localhost, the redirect problem is probably in the network layer in front of Nginx, not in the server block itself. Check that before rewriting a working config.

When to Get Help

Get help from a DevOps engineer if your site sits behind a CDN, cloud load balancer, Kubernetes ingress, or multiple reverse proxies. HTTPS redirects in layered infrastructure depend on where TLS terminates and which headers are trusted.

You should also ask for help before enabling long-term HSTS across many subdomains. A wrong setting can lock users out of services that are not ready for HTTPS.

Redirecting HTTP to HTTPS in Nginx is a small configuration change with a big security impact. Use a dedicated port 80 redirect block, preserve the request URI, verify certificates, and test for loops before calling it done.