Websites & apps

Redirect HTTP to HTTPS in Nginx

Send every plain HTTP visitor to the secure HTTPS version of your site with one return 301 in Nginx, while keeping Let's Encrypt renewals working and avoiding redirect loops.

BeginnerLinux server5 min read

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.

Nginx
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;
}
  • $host is the hostname the visitor asked for, so www stays www and the bare domain stays bare.
  • $request_uri keeps the full path and query string.
  • 301 tells 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:

Nginx
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:

Bash
sudo mkdir -p /var/www/letsencrypt

Then point Certbot at it with --webroot -w /var/www/letsencrypt. The details are in Certbot webroot and standalone modes.

Apply the change

Bash
sudo nginx -t && sudo systemctl reload nginx

nginx -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:

Nginx
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:
Nginx
  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:

Bash
curl -I http://example.com/about?ref=test

Expected result:

HTTP/1.1 301 Moved Permanently
Location: https://example.com/about?ref=test

Follow the chain to confirm it ends in a 200 with no loop:

Bash
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-run and 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_name in the SSL block.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.