Once your site has a certificate, every visitor should land on HTTPS. Browsers mark plain HTTP pages as "Not secure", and logins or forms sent over HTTP can be read in transit. In Nginx the redirect is a short port 80 server block with return 301. This guide sets it up, keeps Let's Encrypt renewals working, and covers the setups where redirects loop.
Before you start
- A Linux VPS with Nginx serving your site.
- A working certificate for the domain. If you do not have one, follow Free SSL for Nginx with Let's Encrypt first.
- Ports 80 and 443 open in your firewall. On Ubuntu with UFW:
sudo ufw allow 'Nginx Full'. On AlmaLinux or Rocky:sudo firewall-cmd --permanent --add-service=http --add-service=https && sudo firewall-cmd --reload.
Warning: Do not redirect to HTTPS until https://example.com already loads without a certificate warning. Otherwise every visitor is sent to a broken page.
The basic redirect
Split your site into two server blocks: one on port 80 that only redirects, and one on port 443 that serves the site.
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
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;
root /var/www/example.com/public;
index index.html index.php;
}$hostis the hostname the visitor asked for, sowwwstayswwwand the bare domain stays bare.$request_urikeeps the full path and query string.301tells browsers and search engines the move is permanent.
If you also want one canonical hostname, replace $host with it, for example https://example.com$request_uri. See Redirect www to non-www in Nginx.
Note: If you ran certbot --nginx and chose the redirect option, Certbot already added a port 80 block with an if ($host =...) test. It works. You can replace it with the simpler block above, but keep only one redirect per hostname.
Keep Let's Encrypt renewals working
Certbot's --nginx plugin handles renewals itself, so the plain redirect is enough. If you use the webroot method, Let's Encrypt reaches http://example.com/.well-known/acme-challenge/... on port 80. It follows redirects to HTTPS, but a broken HTTPS config would then block renewal. Serving that path directly over HTTP is the safest choice:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/letsencrypt;
default_type text/plain;
}
location / {
return 301 https://$host$request_uri;
}
}Create the folder once:
sudo mkdir -p /var/www/letsencryptThen point Certbot at it with --webroot -w /var/www/letsencrypt. The details are in Certbot webroot and standalone modes.
Apply the change
sudo nginx -t && sudo systemctl reload nginxnginx -t checks the syntax first. The reload only runs if the test passes.
Add HSTS once everything works
HSTS tells browsers to use HTTPS for your domain without even trying HTTP first. Add it inside the 443 server block:
add_header Strict-Transport-Security "max-age=31536000" always;Warning: Browsers remember HSTS for the full max-age. If your certificate later breaks, visitors cannot click through the warning. Start with a short value such as max-age=300, confirm everything works, then raise it. Only add includeSubDomains if every subdomain has HTTPS.
Avoid redirect loops
A loop happens when HTTPS requests reach Nginx as plain HTTP, get redirected, and arrive as HTTP again. Common causes:
- Cloudflare "Flexible" SSL: Cloudflare talks to your server over HTTP even when the visitor uses HTTPS. Your port 80 block redirects, Cloudflare fetches over HTTP again, and so on. Set Cloudflare's SSL mode to Full (strict) once your server has a valid certificate.
- A load balancer or proxy ending SSL: the proxy forwards plain HTTP to Nginx. Redirect based on the header the proxy sends instead:
if ($http_x_forwarded_proto = "http") {
return 301 https://$host$request_uri;
}- The app also forces HTTPS: WordPress or Laravel may redirect based on what PHP sees. Pass
fastcgi_param HTTPS on;in your PHP location inside the 443 block if your config does not already include it.
Check it works
Look at the headers on the HTTP version:
curl -I http://example.com/about?ref=testExpected result:
HTTP/1.1 301 Moved Permanently
Location: https://example.com/about?ref=testFollow the chain to confirm it ends in a 200 with no loop:
curl -sIL http://example.com/ | grep -iE "^HTTP|^location"In a browser, the padlock should appear and the address bar should show https://.
Troubleshooting
- ERR_TOO_MANY_REDIRECTS: see the loop causes above, or the full guide Fix ERR_TOO_MANY_REDIRECTS on Nginx.
- HTTPS times out but HTTP redirects: port 443 is closed in the firewall. Open it and retry.
- Certificate renewal fails after adding the redirect: HTTPS is broken or the webroot path is wrong. Run
sudo certbot renew --dry-runand read the error. - Some pages show "Not secure" with a padlock warning: the page loads images or scripts over
http://. That is mixed content, not a redirect problem. - The default Nginx page appears on HTTPS: no 443 block matches the hostname. Check
server_namein the SSL block.
Next steps
- Strengthen the HTTPS side: Add security headers in Nginx.
- Fix pages that still load insecure files: Fix mixed content after enabling SSL.
