Every website should serve all traffic over HTTPS. Browsers display “Not Secure” warnings for HTTP sites. Google uses HTTPS as a ranking signal. Payment processing requires it. User credentials transmitted over HTTP are visible to anyone on the network. The redirect from HTTP to HTTPS is one of the first things to configure on any new server.
In Nginx, this redirect is a few lines of configuration. Getting it wrong, however, creates redirect loops, mixed content errors, or SSL connection failures that make the site unreachable. This guide covers the correct configuration, the common mistakes, and how to verify everything works.
Why HTTPS matters#
Security
HTTP transmits everything in plaintext. Every request URL, every form submission, every cookie, every piece of content is visible to anyone who can observe the network traffic between the browser and the server. On public Wi-Fi, a corporate network, or any network you do not control, this means passwords, session tokens, and personal data are exposed.
HTTPS encrypts the entire connection. The browser and server negotiate a TLS session before any data is exchanged. Everything after the handshake is encrypted and cannot be read or modified by a third party.
SEO
Google confirmed HTTPS as a ranking signal in 2014. The weight is not massive, but it exists. More importantly, Chrome and other browsers display “Not Secure” next to the URL for HTTP sites. Visitors who see this warning are more likely to leave. The indirect SEO impact from increased bounce rate is potentially larger than the direct ranking signal.
Browser behavior
Modern browsers are increasingly aggressive about HTTPS:
- Chrome shows “Not Secure” in the address bar for any HTTP page
- Firefox shows a crossed-out lock icon
- Some browsers block mixed content (HTTP resources loaded on an HTTPS page) entirely
- Features like geolocation, camera access, service workers, and HTTP/2 require HTTPS
Compliance
PCI DSS requires HTTPS for any page that handles payment data. HIPAA requires encryption in transit for health information. GDPR requires appropriate security measures for personal data. For most businesses, HTTPS is not optional.
The Nginx redirect configuration#
Basic redirect
The standard approach uses a separate server block for port 80 that redirects all traffic to HTTPS:
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
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;
# ... rest of your HTTPS configuration
}
The first server block listens on port 80 (HTTP) and issues a 301 permanent redirect to the HTTPS version of the same URL. The
$host
variable preserves the original hostname (example.com or www.example.com). The
$request_uri
variable preserves the full path and query string.
return 301
is the correct directive for this redirect. It is faster than a
rewrite
rule because Nginx processes it immediately without evaluating regex patterns.
Why 301 and not 302
A 301 redirect is permanent. It tells browsers and search engines that the HTTP URL should never be used again. The browser caches the redirect and goes directly to HTTPS on subsequent visits. Search engines transfer ranking value from the HTTP URL to the HTTPS URL.
A 302 redirect is temporary. Browsers do not cache it. Search engines keep the HTTP URL in their index. Using 302 for an HTTP-to-HTTPS redirect is a common mistake that splits SEO signals between the HTTP and HTTPS versions of every page.
Redirect with www normalization
Most sites want to redirect both HTTP-to-HTTPS and www-to-non-www (or vice versa) in a single hop:
# Redirect HTTP to HTTPS and www to non-www
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
# Redirect HTTPS www to non-www
server {
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;
}
# Main site (HTTPS, non-www)
server {
listen 443 ssl;
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/html;
# ... rest of configuration
}
This configuration handles four cases:
-
http://example.comredirects tohttps://example.com(one hop) -
http://www.example.comredirects tohttps://example.com(one hop) -
https://www.example.comredirects tohttps://example.com(one hop) -
https://example.comserves the site (no redirect)
Note that both the www HTTPS block and the main block need SSL certificates. The www block must complete the TLS handshake before it can issue the redirect. If the certificate does not cover
www.example.com
, visitors to that URL see an SSL error instead of the redirect.
Using rewrite instead of return
Some older guides use
rewrite
for the redirect:
server {
listen 80;
server_name example.com;
rewrite ^(.*)$ https://example.com$1 permanent;
}
This works but is less efficient than
return 301
. The
rewrite
directive evaluates a regex pattern on every request. The
return
directive does not need regex evaluation. For a simple redirect,
return 301
is the better choice.
Common mistakes#
Redirect loop
The most common mistake. This happens when the HTTPS server block also contains a redirect to HTTPS, or when a reverse proxy or CDN in front of Nginx sends requests to the backend over HTTP.
Symptom: The browser shows “too many redirects” or ERR_TOO_MANY_REDIRECTS.
Cause 1: Both blocks redirect. If you put the redirect inside the same server block that handles both HTTP and HTTPS:
# WRONG - this creates a loop
server {
listen 80;
listen 443 ssl;
server_name example.com;
# This redirects ALL requests, including HTTPS ones
return 301 https://example.com$request_uri;
}
The fix is to use separate server blocks for port 80 and port 443, as shown in the basic configuration above.
Cause 2: Reverse proxy or CDN. If Cloudflare, a load balancer, or another reverse proxy sits in front of Nginx, the proxy may terminate SSL and forward requests to Nginx over HTTP. Nginx sees an HTTP request and redirects to HTTPS. The proxy follows the redirect, sends another HTTP request to Nginx, and the loop continues.
The fix depends on the proxy. For Cloudflare, set the SSL mode to “Full” or “Full (Strict)” so Cloudflare connects to Nginx over HTTPS. For other proxies, check the
X-Forwarded-Proto
header:
server {
listen 80;
server_name example.com;
# Only redirect if the original request was HTTP
if ($http_x_forwarded_proto = "http") {
return 301 https://$host$request_uri;
}
}
Cause 3: WordPress forcing HTTPS independently. If WordPress has its own HTTPS redirect (through a plugin or wp-config.php) on top of the Nginx redirect, the two can conflict. Let Nginx handle the redirect and remove any HTTPS redirect plugins from WordPress.
Mixed content after enabling HTTPS
The redirect works but the site looks broken. Images are missing, styles are not applied, or the browser shows a warning about insecure content.
This happens when the site’s HTML contains hardcoded
http://
URLs for images, stylesheets, scripts, or other resources. The page loads over HTTPS but tries to load resources over HTTP. Browsers block or warn about this mixed content.
The fix for WordPress:
- Update
siteurlandhomein Settings > General to usehttps:// - Run a search-and-replace in the database to change all
http://yourdomain.comreferences tohttps://yourdomain.com - Use WP-CLI for a serialization-safe replacement:
wp search-replace 'http://example.com' 'https://example.com'
SSL certificate not covering all domains
The redirect sends traffic from
http://www.example.com
to
https://www.example.com
, but the SSL certificate only covers
example.com
(without www). The browser shows an SSL error for the www version.
The fix: issue a certificate that covers both domains. With certbot:
certbot certonly --nginx -d example.com -d www.example.com
For a complete guide to certificate management, see How to install an SSL certificate.
Using $server_name instead of $host
# Potentially wrong
return 301 https://$server_name$request_uri;
$server_name
uses the first value in the
server_name
directive. If your server block has
server_name example.com www.example.com
,
$server_name
is always
example.com
, even if the visitor requested
www.example.com
. This works if you want to normalize to non-www, but it is confusing if you expect it to preserve the original hostname. Use
$host
if you want to preserve the hostname the visitor used.
Not preserving the request URI
# Wrong - loses the path and query string
return 301 https://example.com;
This redirects every URL to the homepage. A visitor requesting
http://example.com/blog/article?id=123
ends up at
https://example.com/
instead of
https://example.com/blog/article?id=123
. Always include
$request_uri
to preserve the full path and query string.
Testing the redirect#
With curl
# Check the redirect (do not follow it)
curl -I http://example.com
Expected output:
HTTP/1.1 301 Moved Permanently
Location: https://example.com/
Verify that:
- The status code is 301 (not 302)
- The Location header points to the HTTPS version
- The Location includes the correct hostname
Test with a path:
curl -I http://example.com/blog/article
The Location header should include the full path:
https://example.com/blog/article
.
Check for redirect chains
curl -ILs http://www.example.com 2>&1 | grep -E "^HTTP|^location"
This follows all redirects and shows each hop. Ideally you see one redirect (HTTP to HTTPS). If you see two or more, you have a chain that should be consolidated.
Check in browser developer tools
Open the Network tab (F12), visit the HTTP URL, and look at the first request. It should show a 301 status with the HTTPS URL in the Location header. If you see multiple 301/302 redirects before the page loads, you have a chain.
HSTS: enforcing HTTPS at the browser level#
The redirect handles visitors who type
http://
or follow an HTTP link. But the first request is still over HTTP before the redirect kicks in. HSTS (HTTP Strict Transport Security) eliminates this by telling the browser to always use HTTPS for your domain, even if the user types
http://
.
Add the HSTS header in your HTTPS server block:
server {
listen 443 ssl;
server_name example.com;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# ... rest of configuration
}
The
max-age=31536000
tells the browser to remember the HSTS policy for one year (in seconds). After the first visit over HTTPS, the browser automatically upgrades all future HTTP requests to HTTPS without making the initial HTTP request at all.
Only enable HSTS after you have verified that HTTPS works correctly. HSTS is difficult to undo. If you enable it and then discover an SSL problem, browsers will refuse to connect over HTTP, and the cached HSTS policy can persist for the full max-age period. Start with a short max-age (like 300 seconds) for testing, then increase to a year once everything is confirmed working.
The
includeSubDomains
flag applies the policy to all subdomains. Only use this if all subdomains have valid SSL certificates and serve over HTTPS.
HTTPS and HTTP/2#
HTTP/2 requires HTTPS. Once your redirect is in place and all traffic is served over HTTPS, enable HTTP/2 in Nginx:
server {
listen 443 ssl http2;
server_name example.com;
# ...
}
HTTP/2 provides significant performance improvements: multiplexed requests, header compression, and server push. These are free performance gains that only require HTTPS and the
http2
parameter on the
listen
directive.
How Hostney handles HTTPS redirects#
On Hostney, HTTPS is enabled automatically for every site. SSL certificates are provisioned via Let’s Encrypt when the domain is added, and the HTTP-to-HTTPS redirect is configured at the server level. You do not need to edit Nginx configuration files or set up certbot manually.
The redirect uses a 301 status code with the full URI preserved. The SSL configuration uses TLS 1.2 and 1.3 with modern cipher suites. HSTS is not enabled by default but can be configured through the control panel’s security headers settings for each domain.
For sites migrated to Hostney from a host that did not use HTTPS, the main step after migration is updating the database URLs from
http://
to
https://
. This can be done from the control panel’s Search & Replace tab or via WP-CLI over SSH.