Every production site I deploy ends up with the same rule sitting near the top of its configuration: anything arriving over plain HTTP gets bounced straight to HTTPS. It sounds like a trivial thing to configure, and mechanically it is, but I’ve seen this done wrong often enough — redirect loops, mixed content warnings, SEO-damaging redirect chains — that I want to walk through it properly instead of just dropping a snippet and moving on.
In this guide, I’ll cover the correct way to force HTTPS in Nginx, how to handle the www vs non-www decision at the same time, what changes when Nginx sits behind a load balancer or CDN, and the mistakes that quietly hurt SEO or break functionality.
Why Redirect HTTP to HTTPS at All
If a certificate is installed and HTTPS works, but I don’t force it, visitors and search engines will still happily use plain HTTP for a chunk of traffic — old bookmarks, typed URLs without https://, links from other sites. That means:
- Credentials and session cookies transmitted in plaintext on some fraction of visits
- Search engines potentially indexing both HTTP and HTTPS versions as duplicate content
- No consistent way to enable HSTS (HTTP Strict Transport Security), which requires HTTPS to be the canonical, enforced version
A redirect closes this gap in one place, at the web server, rather than relying on the application or every individual page to handle it.
Requirements
- Nginx installed and running
- A working SSL/TLS certificate already installed (see my articles on self-signed certificates or enabling SSL/TLS with Let’s Encrypt)
- Access to edit your site’s Nginx server block configuration
Step 1: Confirm HTTPS Already Works
Before forcing every visitor onto HTTPS, I make sure it actually works first. I check manually:
curl -Ik https://example.com/
I want to see HTTP/1.1 200 OK or HTTP/2 200 with no certificate errors. If HTTPS isn’t working yet, I fix that first — redirecting to a broken HTTPS endpoint locks visitors out entirely.
Step 2: Configure the Redirect
The cleanest, most widely recommended approach is a dedicated server block listening on port 80 that does nothing but redirect, plus a separate server block for port 443 that serves the actual site.
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
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;
location / {
try_files $uri $uri/ =404;
}
}
A few details matter here:
return 301notrewrite. A301 Moved Permanentlyis correct for a permanent protocol change — it tells browsers and search engines to update their records and stop requesting the HTTP version. I avoid using arewrite ^ https://...directive for this, sincereturnis simpler, faster, and less error-prone.$hostnot$server_name. Using$hostpreserves whatever hostname the visitor actually typed (includingwww.if present), while$server_namewould always resolve to the first name listed inserver_name, potentially causing unwanted host rewriting.$request_uripreserves the full original path and query string, so a request to/blog/post?ref=twitterredirects to the equivalent HTTPS URL rather than dropping the path.
Handling www vs Non-www
I like to make a firm decision about canonical domain format — either www.example.com or example.com — and redirect everything else to it, combined with the HTTPS redirect in a single hop where possible, to avoid a redirect chain (HTTP www → HTTPS www → HTTPS non-www, which is three hops instead of one).
Redirecting Non-www to www, with HTTPS
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://www.example.com$request_uri;
}
server {
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;
return 301 https://www.example.com$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name 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;
}
This collapses both the protocol and the domain decision into a single redirect from any entry point, which is the SEO-friendliest approach — one hop, one canonical destination.
Complete Example Configuration
Here’s a full setup I’d actually deploy, redirecting HTTP to HTTPS and non-www to www, with HSTS enabled once I’m confident everything works:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://www.example.com$request_uri;
}
server {
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;
return 301 https://www.example.com$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
root /var/www/example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
Testing the Redirect
curl -I http://example.com/
Expected response:
HTTP/1.1 301 Moved Permanently
Location: https://www.example.com/
I also test with a path and query string to confirm they’re preserved:
curl -I "http://example.com/blog/post?ref=test"
Location: https://www.example.com/blog/post?ref=test
Finally, I load the site in an actual browser and check the address bar shows https:// after the redirect, with no mixed-content warnings in the console.
Troubleshooting Common Issues
ERR_TOO_MANY_REDIRECTS / redirect loop. This is the most common HTTP-to-HTTPS mistake, and it almost always comes from one of two causes:
- Nginx sits behind a load balancer, CDN, or reverse proxy that terminates SSL, so requests reach Nginx over plain HTTP even for HTTPS visitors. Nginx’s
return 301 https://...then redirects a request that was “already HTTPS” from the browser’s perspective, looping forever. I fix this by checking$http_x_forwarded_protoinstead of blindly redirecting:
server {
listen 80;
server_name example.com;
if ($http_x_forwarded_proto != "https") {
return 301 https://$host$request_uri;
}
root /var/www/example.com;
}
- Cloudflare or another CDN is set to “Flexible SSL” while Nginx also forces HTTPS — Cloudflare talks HTTP to the origin, Nginx redirects to HTTPS, Cloudflare receives the redirect and requests HTTP again. The fix here is setting the CDN’s SSL mode to “Full” or “Full (strict)” so it talks HTTPS to the origin too.
Mixed content warnings after redirecting. This means some assets (images, scripts, stylesheets) are still hardcoded with http:// URLs in the HTML/CSS/JS itself. The redirect only affects the initial page load; it can’t rewrite asset URLs embedded in the page. I fix these at the source, or use sub_filter in Nginx as a stopgap for HTML rewriting if I can’t immediately fix the codebase.
Redirect doesn’t preserve the URL path. This happens if I use a hardcoded destination like return 301 https://example.com/; instead of including $request_uri. Always include it unless there’s a deliberate reason to send everyone to the homepage.
Security Considerations
- Once HTTPS is fully working and redirecting correctly, enable HSTS (
Strict-Transport-Securityheader) so browsers refuse to even attempt an HTTP connection on future visits, closing the door on downgrade attacks entirely. - Be cautious with
includeSubDomainsin HSTS — it applies the policy to every subdomain, so make sure all of them support HTTPS before turning it on, or you’ll break any subdomain still running plain HTTP. - Consider HSTS preloading (submitting your domain to the browser preload list) only after you’re fully confident in your HTTPS setup across all subdomains, since it’s difficult to reverse quickly.
- Never redirect HTTPS traffic back to HTTP under any circumstance — that direction defeats the entire purpose and exposes users unnecessarily.
Performance Tips
- Use
return 301rather thanrewritefor redirects —returnis evaluated earlier in the request-processing cycle and avoids the overhead of the regex engine. - Keep the HTTP-to-HTTPS redirect in its own lightweight
serverblock with nothing else in it, so Nginx handles it as fast as possible without evaluating other location blocks first. - Combine domain and protocol redirects into a single hop, as shown above, so browsers and crawlers only need one round trip instead of chaining multiple 301s.
Real-World Use Cases
- Standard production deployment for any public-facing website that has a valid SSL certificate.
- Enforcing HTTPS across an entire domain and its
wwwvariant with a single canonical destination for SEO consistency. - API endpoints where you want to guarantee TLS is always used, rejecting or redirecting any accidental plaintext client requests.
- Migrating a legacy HTTP-only site to HTTPS gradually, using redirects while updating hardcoded asset URLs in the background.
Best Practices
- Confirm HTTPS actually works correctly before enabling the redirect.
- Use
return 301 https://$host$request_uri;as the standard pattern — it’s fast, correct, and preserves paths. - Decide on
wwwvs non-wwwearly and redirect everything to a single canonical form in one hop. - Watch for redirect loops caused by proxies/CDNs terminating SSL — use
$http_x_forwarded_protoin those setups. - Add HSTS only after confirming the redirect and certificate setup are fully stable.
- Test with
curl -Iafter every change, not just in the browser, since browsers cache redirects aggressively and can mask a broken config.
Wrapping Up
Forcing HTTPS is one of those changes that takes five minutes to write and can take an hour to debug if the redirect logic is wrong — usually because of a proxy in front of Nginx that I forgot to account for. Once it’s correctly in place, paired with HSTS, it closes off an entire class of downgrade and interception risks with almost no ongoing maintenance. I treat it as a non-negotiable part of any production deployment, right alongside the SSL certificate itself.