The short answer: add a server block on port 80 that sends every request to the HTTPS version of the same URL.
nginx
1server {2 listen 80;3 listen [::]:80;4 server_name example.com www.example.com;5
6 return 301 https://$host$request_uri;7}$host is the name the visitor asked for, and $request_uri is the path with the query string, so http://example.com/blog/?page=2 goes to https://example.com/blog/?page=2. This only works if Nginx already serves the site over HTTPS. The rest of this guide shows the complete setup and how to check it with curl.
All configs below were tested on Ubuntu 22.04 (Nginx 1.18), 24.04 (Nginx 1.24) and 26.04 (Nginx 1.28) with a certificate from a test ACME server.
Why redirect HTTP to HTTPS
HTTPS is HTTP over TLS. It encrypts the traffic between the browser and your server, so nobody on the way can read or change it. Browsers mark plain HTTP pages as "Not secure", and Google uses HTTPS as a ranking signal.
Getting a certificate is only half of the job. As long as the site still answers on http://, people who type your domain without the scheme, old links and bookmarks keep landing on the unencrypted version. A permanent redirect sends all of them to HTTPS and tells search engines which address to index.
What you need
- A server with Nginx. On Ubuntu,
sudo apt install nginxis enough. - A domain with DNS records pointing to the server. In this guide, that's
example.comandwww.example.com. - Ports 80 and 443 open in ufw and in your cloud firewall or security group. Port 80 must stay open after the switch: that's where the redirect lives.
- A TLS certificate that covers every name you serve, both
example.comandwww.example.com.
You don't have to buy a certificate. For most sites, a free domain-validated certificate from Let's Encrypt is all you need, and Certbot renews it automatically.
Paid OV and EV certificates from CAs like Sectigo, DigiCert or GlobalSign make sense when a contract or a compliance policy asks for them. Browsers show the same padlock for all three types.
Set up the redirect step by step
The steps below take a server from plain HTTP to a full HTTPS setup with one canonical address. If you already have a working HTTPS server block, jump to step 4 and compare it with yours.
Step 1: Redirect HTTP and keep the ACME challenge path open
Start with the port 80 block. It redirects everything to HTTPS, except the path Let's Encrypt uses to check that you control the domain. Create /etc/nginx/sites-available/example.com:
nginx
1server {2 listen 80;3 listen [::]:80;4 server_name example.com www.example.com;5
6 location ^~ /.well-known/acme-challenge/ {7 root /var/www/letsencrypt;8 }9
10 location / {11 return 301 https://example.com$request_uri;12 }13}The target is hardcoded as https://example.com, so a visitor who comes to http://www.example.com gets to the final address in one redirect, not two. If www.example.com is your main address, put it there instead.
Create the directories and a test page (replace it with your site later), disable Ubuntu's default site, enable this one, and reload Nginx:
shell
1sudo mkdir -p /var/www/letsencrypt /var/www/example.com2echo '<h1>example.com works over HTTPS</h1>' | sudo tee /var/www/example.com/index.html3sudo rm /etc/nginx/sites-enabled/default4sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/5sudo nginx -t6sudo systemctl reload nginxStep 2: Get a certificate
With port 80 in place, install Certbot from the Ubuntu repository and request a certificate for both names. Certbot puts the challenge file in /var/www/letsencrypt, and the location from step 1 serves it:
shell
1sudo apt install certbot2sudo certbot certonly --webroot -w /var/www/letsencrypt -d example.com -d www.example.com --deploy-hook "systemctl reload nginx"The certificate lands in /etc/letsencrypt/live/example.com/. If you want Certbot to edit the Nginx config for you, or prefer the snap version, follow our guide on how to install Let's Encrypt for Nginx on Ubuntu.
If you already have a certificate from another CA, skip this step and use the paths to your fullchain.pem (certificate plus intermediates) and private key in step 4.
Step 3: Add modern TLS settings
Put the TLS settings in a snippet, so every HTTPS server block can include the same file.
These values come from the "intermediate" profile of the TLSRef Configurator, the successor of the Mozilla SSL Configuration Generator. It allows TLS 1.2 and TLS 1.3 only and still works with old clients like Internet Explorer 11 on Windows 10, Safari 9 and Android 4.4.
Create /etc/nginx/snippets/ssl-params.conf:
nginx
1ssl_protocols TLSv1.2 TLSv1.3;2ssl_ecdh_curve X25519:prime256v1:secp384r1;3ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;4ssl_prefer_server_ciphers off;5
6ssl_session_timeout 1d;7ssl_session_cache shared:MozSSL:10m;Keep ssl_ciphers on one line. If you split the list over several lines to make it readable, Nginx passes the line breaks to OpenSSL, and nginx -t fails with SSL_CTX_set_cipher_list(...) failed (SSL: error:0A000118:SSL routines::invalid command).
Don't drop this file into /etc/nginx/conf.d/ either. Ubuntu's nginx.conf already sets ssl_protocols and ssl_prefer_server_ciphers at the http level, and the second copy fails with "ssl_prefer_server_ciphers" directive is duplicate. Inside a server block, the snippet overrides those defaults.
On Ubuntu 26.04 and other systems with OpenSSL 3.5 or newer, you can add the post-quantum hybrid group in front: ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1:secp384r1;. Older OpenSSL (3.0 on Ubuntu 22.04 and 24.04) rejects it with group 'X25519MLKEM768' cannot be set.
Step 4: Configure HTTPS and choose www or non-www
Now replace /etc/nginx/sites-available/example.com with the full config. It has three server blocks: the redirect on port 80 from step 1, an HTTPS block for www.example.com that only redirects, and the HTTPS block that serves the site.
nginx
1# HTTP: send every request to https://example.com2server {3 listen 80;4 listen [::]:80;5 server_name example.com www.example.com;6
7 location ^~ /.well-known/acme-challenge/ {8 root /var/www/letsencrypt;9 }10
11 location / {12 return 301 https://example.com$request_uri;13 }14}15
16# HTTPS on www: send to the non-www host17server {18 listen 443 ssl;19 listen [::]:443 ssl;20 server_name www.example.com;21
22 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;23 ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;24 include snippets/ssl-params.conf;25
26 return 301 https://example.com$request_uri;27}28
29# HTTPS on example.com: the site itself30server {31 listen 443 ssl;32 listen [::]:443 ssl;33 server_name example.com;34
35 ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;36 ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;37 include snippets/ssl-params.conf;38
39 root /var/www/example.com;40 index index.html;41}The www block on port 443 matters. Without it, anyone who opens https://www.example.com directly, or follows an old link to it, gets the site on a second address. With it, every combination of http, https, www and non-www ends up on https://example.com in one hop.
To make www.example.com the main address, swap the names: the middle block gets server_name example.com;, and all three return lines point to https://www.example.com$request_uri.
There's no ssl on; in this config. That directive was removed in Nginx 1.25.1, and the ssl parameter of listen does the job on all versions.
Test and reload:
shell
1sudo nginx -t2sudo systemctl reload nginxStep 5: Turn on HTTP/2
How you enable HTTP/2 depends on your Nginx version, so check it first:
shell
1nginx -vOn Nginx 1.25.1 and newer (Ubuntu 26.04 ships 1.28), add one line to each HTTPS server block, for example after the include line:
nginx
1http2 on;On older versions (1.18 on Ubuntu 22.04, 1.24 on Ubuntu 24.04), there's no http2 directive, and nginx -t fails with unknown directive "http2". Add http2 to the listen lines of the HTTPS blocks instead:
nginx
1listen 443 ssl http2;2listen [::]:443 ssl http2;This older form still works on new versions, but Nginx 1.25.1+ prints the "listen ... http2" directive is deprecated, use the "http2" directive instead. Run sudo nginx -t and sudo systemctl reload nginx again after the change.
Step 6: Check the redirect with curl
Don't test redirects in your main browser: it caches a 301, and after a config change you may keep seeing the old result. Use curl:
shell
1curl -I http://example.com/2curl -I "http://www.example.com/blog/?page=2"3curl -I https://www.example.com/4curl -I https://example.com/The first three should return a 301 with the right Location, and the last one should return 200. Over HTTP/2, curl prints header names in lowercase, so the HTTPS answers look slightly different:
shell
1HTTP/1.1 301 Moved Permanently2Location: https://example.com/3
4HTTP/1.1 301 Moved Permanently5Location: https://example.com/blog/?page=26
7HTTP/2 3018location: https://example.com/9
10HTTP/2 200To see the whole chain from the first request to the final page, add -L. One 301 followed by 200 is what you want:
shell
1curl -sIL http://www.example.com/ | grep -iE '^(HTTP|location)'To check the certificate and the TLS version, run curl -vI https://example.com/ and look for SSL connection using TLSv1.3, a subjectAltName line that matches your domain, and SSL certificate verify ok (newer curl versions print SSL certificate verified via OpenSSL instead).
Step 7: Add HSTS (optional, but recommended)
A 301 still leaves the very first request on plain HTTP. HSTS (HTTP Strict Transport Security) closes that gap: after one visit, the browser goes straight to HTTPS for your domain and doesn't even try port 80.
Start with a short lifetime. Add this line to both HTTPS server blocks, next to the include line:
nginx
1add_header Strict-Transport-Security "max-age=300" always;always adds the header to every response, including redirects and errors. Reload Nginx and check:
shell
1curl -sI https://example.com/ | grep -i strict-transport-securityshell
1Strict-Transport-Security: max-age=300When everything works over HTTPS for a few days, raise the lifetime to the value TLSRef recommends, two years:
nginx
1add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;Be careful with two things:
includeSubDomainscovers every subdomain. Add it only when all subdomains, including internal ones, work over HTTPS. Browsers that saw the header won't open the others over HTTP untilmax-ageruns out.add_headerin alocationcancels the server-level headers. If a location has its ownadd_header, for exampleCache-Controlfor static files, Nginx drops HSTS from those responses. In the test,/assets/withadd_header Cache-Controlcame back without the HSTS header. Repeat the HSTS line inside such locations.
HSTS preload (the preload flag and a submission to the browsers' preload list) is a separate, hard-to-undo step. Read the requirements on the HSTS preload site before you add it. More about the header is on MDN.
Redirect all sites on the server at once
If one Nginx serves many domains, you don't need a port 80 block for each. One default server can redirect all of them:
nginx
1server {2 listen 80 default_server;3 listen [::]:80 default_server;4 server_name _;5
6 location ^~ /.well-known/acme-challenge/ {7 root /var/www/letsencrypt;8 }9
10 location / {11 return 301 https://$host$request_uri;12 }13}return 301 with $host sends every visitor to the HTTPS version of whatever name they asked for. In the test, a request for http://shop.example.org/cart?item=1 went to https://shop.example.org/cart?item=1. Each of those names still needs its own HTTPS server block and certificate.
The catch-all keeps the host name, so it can't pick www or non-www for you. http://www.example.com first goes to https://www.example.com, and the www block on port 443 from step 4 sends it on to https://example.com.
That's two redirects instead of one. It works, but for your main sites, a dedicated port 80 block with a hardcoded target is cleaner.
Only one server can be default_server on a port. Ubuntu's default site already is, so remove /etc/nginx/sites-enabled/default first, or nginx -t fails with a duplicate default server for 0.0.0.0:80.
Also remove the port 80 blocks from the individual sites. A site's own block with a matching server_name wins over the default server.
Use $host here, not $server_name. $server_name is the first name in the server_name directive, which is _ in this block, and the redirect turns into https://_/cart.
301 or 308?
Both are permanent redirects, and search engines treat them the same way. The difference is in how clients repeat the request:
- 301: browsers and curl turn a POST into a GET when they follow the redirect, and the form data is lost. In the test,
curl -L -d a=1to the HTTP address arrived at the HTTPS page as GET. - 308: the client repeats the same method with the same body. The same test arrived as POST.
For a regular website, 301 is the standard choice: forms are already on HTTPS pages, so nothing posts to HTTP. If an API, webhook or old client may still send POST requests to http://, use return 308. The TLSRef Configurator uses 308 in its sample config for that reason.
Use 302 only for a temporary redirect. Browsers don't cache it by default, and search engines keep the old address in the index.
Behind a load balancer or Cloudflare: fix the redirect loop
If TLS ends on a load balancer (AWS ALB, Cloudflare in Flexible mode, another Nginx), your server receives every request over plain HTTP, even when the visitor uses HTTPS. A port 80 block with return 301 then redirects the HTTPS visitor to HTTPS again, forever. Browsers show ERR_TOO_MANY_REDIRECTS, and curl stops with:
shell
1curl: (47) Maximum (50) redirects followedThe simplest fix is to do the redirect on the load balancer itself: an HTTP-to-HTTPS listener rule on AWS ALB, or "Always Use HTTPS" with Full (strict) mode in Cloudflare. Then the backend serves the site on port 80 and doesn't redirect at all.
If the redirect has to stay in Nginx, check the X-Forwarded-Proto header that the load balancer adds. It tells you which scheme the visitor used:
nginx
1server {2 listen 80;3 server_name example.com;4 root /var/www/example.com;5
6 if ($http_x_forwarded_proto = "http") {7 return 301 https://$host$request_uri;8 }9}In the test behind a TLS-terminating proxy, HTTPS requests got 200 and HTTP requests got a single 301. Only trust this header when the backend is reachable from the load balancer and nowhere else.
Common mistakes
- Using
rewriteorif ($scheme = http)for the redirect. Both work, but they're slower and harder to read. A separate port 80 server withreturnis what the Nginx docs intend for this. - Redirecting to
$server_namein a block with several names. You always land on the first name, and withserver_name _;the redirect breaks. Use$hostor a hardcoded canonical host. - Forgetting the www block on port 443. The HTTP redirect works, but
https://www.example.comstays a duplicate of your site. - A certificate without
www. If the certificate covers onlyexample.com, visitors tohttps://www.example.comsee a certificate error before Nginx can redirect them. - Certbot's redirect left next to yours.
certbot --nginxadds its own port 80 block withif ($host = ...)andreturn 301. That's fine on its own, but don't keep two blocks with the sameserver_nameon port 80: Nginx warns about a conflicting server name and uses only the first one. - Testing in the browser after a 301. Browsers cache permanent redirects. Use curl or a private window.
Wrapping up
The best way to redirect all HTTP requests to HTTPS with Nginx comes down to four pieces:
- one port 80 server with
return 301; - a certificate that covers both www and non-www;
- an HTTPS block that redirects www to your main address;
- modern TLS settings.
Check every combination with curl -I, then turn on HSTS once you're sure HTTPS works everywhere.
If you'd rather not look after certificates, web servers and the rest of your infrastructure yourself, our team can do it for you. Take a look at our cloud engineering services.
