80

I want to create a rule in nginx that does two things:

  1. Removes the "www." from the request URI
  2. Redirects to "https" if the request URI is "http"

There are plenty of examples of how to do each of those things individually, but I can't figure out a solution that does both correctly (i.e. doesn't create a redirect loop and handles all cases properly).

It needs to handle all of these cases:

1. http://www.example.com/path
2. https://www.example.com/path
3. http://example.com/path
4. https://example.com/path

These should all end up at https://example.com/path (#4) without looping. Any ideas?

2
  • 1
    I just redirected www.mydomain.com to mydomain.com at the DNS level and added a 301 for non-https to https in nginx. Seems like that should be fine ¯\_(ツ)_/¯ May 26, 2017 at 23:02
  • 'Removes the "www." from the request URI' - why? This breaks your scalability.
    – symcbean
    May 4, 2021 at 22:35

11 Answers 11

127

The best way to accomplish this is using three server blocks: one to redirect http to https, one to redirect the https www-name to no-www, and one to actually handle requests. The reason for using extra server blocks instead of ifs is that server selection is performed using a hash table, and is very fast. Using a server-level if means the if is run for every request, which is wasteful. Also, capturing the requested uri in the rewrite is wasteful, as nginx already has this information in the $uri and $request_uri variables (without and with query string, respectively).

server {
    server_name www.example.com example.com;
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl;
    ssl_certificate /path/to/server.cert;
    ssl_certificate_key /path/to/server.key;
    server_name www.example.com;
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl;
    ssl_certificate /path/to/server.cert;
    ssl_certificate_key /path/to/server.key;
    server_name example.com;

    <locations for processing requests>
}
12
  • 2
    Is the middle block necessary? Isn't the first block already rewriting from www to non-www? May 16, 2011 at 20:39
  • 6
    The first block only handles http. The middle block is necessary to redirect https requests from https:// www.example.com/ to https:// example.com/. (Sorry for the extra spaces, I can't make it show the https otherwise)
    – kolbyjack
    May 17, 2011 at 1:03
  • 2
    just a minor formatting note - if you want to avoid making a link, you can put comment text inside back-quotes ` , the one under tilde. It would show up like: https://example.com/
    – Cyclops
    Jun 9, 2011 at 21:13
  • 10
    the second block also needs cert info.
    – ricka
    May 6, 2016 at 1:20
  • 3
    Trying this answer, I ran into another problem. Thought I could 301 redirect from www.sub.example.com to sub.example.com and then only obtain an SSL certificate for sub.example.com Now I know that ssl cert check happens before the 301 redirect, so it can not work. More explanation here: serverfault.com/a/358625/144811
    – Gruzzles
    Nov 24, 2016 at 18:08
12

This works for me:

server {
    listen              80;
    server_name         www.yourdomain.com yourdomain.com;
    return              301 https://yourdomain.com$request_uri;
}

server {
    listen              443 ssl;
    server_name         www.yourdomain.com;
    ssl_certificate     /path/to/certificate.crt;
    ssl_certificate_key /path/to/private/key.pem;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    return              301 https://yourdomain.com$request_uri;
}

server {
    listen              443 ssl;
    server_name         yourdomain.com;
    ssl_certificate     /path/to/certificate.crt;
    ssl_certificate_key /path/to/private/key.pem;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;

    # do the proper handling of the request
}

Keep in mind that both yourdomain.com and www.yourdomain.com must be in your SSL certificate. This is possible with a wildcard certificate or with a Server Alternate Name as explained here. Check https://www.startssl.com for nice and free certificates that do this. (Edith: beginning with Chrome version 56, startssl certificates will not be trusted anymore. Try https://letsencrypt.org/ instead.)

3
  • This one actually works, but I thought it could be done in more clear way without a lot of duplicate config lines.
    – zloynemec
    Sep 18, 2017 at 8:48
  • @zloynemec You could put the SSL stuff in a separate .conf file and use the include rule to add it to both SSL server blocks.
    – Person
    Apr 2, 2018 at 9:03
  • Also if you are using cloudflare you need to pay the $10/mo cert to be able to redirect and proxy the 2 subdomains ( www + something ). Let me know if there is a workaround.
    – Freedo
    Sep 24, 2018 at 4:17
9

After spending so much time with hundreds of similar cases, I've come up with the following snippet. It's short and can be easily tweaked to fit anything.

server {
    listen 80;
    listen 443 ssl;
    server_name example.com www.example.com;
    ssl_certificate /path/to/my/certs/example.com/fullchain.pem;
    ssl_certificate_key /path/to/my/certs/example.com/privkey.pem;

    # Redirect to the correct place, if needed
    set $https_redirect 0;
    if ($server_port = 80) { set $https_redirect 1; }
    if ($host ~ '^www\.') { set $https_redirect 1; }
    if ($https_redirect = 1) {
        return 301 https://example.com$request_uri;
    }

    location / {
    # ...
}

Oh but if is evil!

Yes it can be. But it exists for a reason, and should do no harm to those who know how to use it properly. ;)

5
  • I like this, but do you have any data on the performance hit? Thank you!
    – Freedo
    Sep 24, 2018 at 4:17
  • 1
    Honestly I never benchmarked that, but I believe there would be hardly an impact compared to separate rules since the effect is pretty much the same.
    – emyller
    Sep 24, 2018 at 20:38
  • benchmark on redirection? it's not realy pertinent no? (true question, not a troll ^^)
    – Matrix
    Nov 23, 2018 at 4:42
  • @Freedo Using three server blocks is the fastest in terms of performance; I tried to give the complete explanation here. Jun 15, 2022 at 9:50
  • Years later, I see there is a performance difference and it could impact servers that handle a large amount of traffic. Although I'm curious to see numbers. BTW I see some good tips that could maybe minimize this difference here: getpagespeed.com/server-setup/…
    – emyller
    Jun 15, 2022 at 14:28
3

I prefer to return with a response code so the browser knows you are redirecting it to another URL.

server {
    listen   80;
    server_name  www.example.com;

    return 301 https://example.com$request_uri;
}

then another server configurations block for the https

server {
        listen   443 ssl;
        server_name  example.com;
        ...
    }
1

If you have many domains and you are looking for a more generic approach without loosing performance and without listing all domains all the time, check this.

How it works?

  1. listen to 80 and redirect all http to https - including http://www. which will go to https://www.
  2. listen to 433, but only on www server names and redirect to non-www using regular expression
  3. listen to 433 for each of your non-www server name - this is where all the traffic will end up

Is it fast?
Yes! Even though we use RegExp, it's only in the www-versions block which returns 301. So all normal traffic will be handled without any additional processing cost.

  # Redirect everything to HTTPS (including "www")
  server {
    listen 80 default_server;
    listen [::]:80 default_server;
    return 301 https://$host$request_uri;
  }
  
  # Redirect away from "www" versions:
  server {
    listen [::]:443 ssl http2;
    listen 443 ssl http2;
    server_name www.example-1.com
                www.example-2.com
                www.example-3.com;
    # using generic "www" removal      // https://stackoverflow.com/questions/11323735/nginx-remove-www-and-respond-to-both/45676731#45676731
    if ( $host ~ ^www\.(.+)$ ) {
      set $without_www $1;
      rewrite ^ $scheme://$without_www$uri permanent;
    }
    # SSL settings:
    ssl_certificate /etc/letsencrypt/live/example/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/example/privkey.pem; # managed by Certbot
  }
  
  # And finally one "server" block for each of your domains
  server {
    listen [::]:443 ssl http2;
    listen 443 ssl http2;
    server_name example-1.com;
    # SSL settings, etc...
  }
  server {
    server_name example-2.com;
    # ... listen, ssl, etc...
  }
  server {
    server_name example-3.com;
    # ... etc
  }

PS: if you need help with SSL setup, checkout Mozilla SSL Configuration Generator:
https://ssl-config.mozilla.org/

0

how about creating a server block for this purpose:

server{
    listen 80;
    server_name www.example.net example.net;
    rewrite ^(.*) https://example.net$1 permanent;
}

then restarting nginx

1
  • I get a "conflicting server name" error when restarting. Also, that command won't listen on port 443 for SSL and I need to worry about redirecting https://www.example.com to https://example.com as well.
    – Devin
    Apr 11, 2011 at 16:56
0

I think this should work.

On your plain HTTP server definition something like anthonysomerset suggested, that is:

rewrite ^(.*) https://example.net$1 permanent;

Then on your SSL server definition:

if ($host ~ /^www\./) {
  rewrite ^(.*) https://example.net$1 permanent;
}

This way the redirect should only happen once per request no matter which URL the user goes to originally.

2
  • That worked, thanks. I had to change your conditional to if ($host = 'www.example.com') { since your regex wasn't working for me, though. No idea why, as it looks correct.
    – Devin
    Apr 11, 2011 at 18:20
  • Do note that if is evil and it's generally better to use a declarative way.
    – Blaise
    Mar 21, 2015 at 6:35
0

Here's the full example that ended up working for me. The problem was that I didn't have the ssl details (ssl_certificate, etc.) in the www redirect block. Remember to check your logs (sudo tail -f /var/log/nginx/error.log)!

# HTTP — redirect all traffic to HTTPS
server {
    listen 80;
    listen [::]:80 default_server ipv6only=on;
    return 301 https://$host$request_uri;
}

# HTTPS — redirects www to non-www
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name www.example.com;

    # Use the Let's Encrypt certificates
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Include the SSL configuration from cipherli.st
    include snippets/ssl-params.conf;
    return 301 https://example.com$request_uri;
}

# HTTPS — proxy all requests to the app (port 3001)
server {
    # Enable HTTP/2
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com sub.example.com;

    # Use the Let's Encrypt certificates
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Include the SSL configuration from cipherli.st
    include snippets/ssl-params.conf;

    # For LetsEncrypt:
    location ~ /.well-known {
        root /var/www/html;
        allow all;
    }

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-NginX-Proxy true;
        proxy_pass http://localhost:3001;
        proxy_ssl_session_reuse off;
        proxy_set_header Host $http_host;
        proxy_cache_bypass $http_upgrade;
        proxy_redirect off;
    }
}
0
server {
        listen 80;
        listen 443 ssl;
        server_name devly.co www.devly.co;

        ssl on;
        ssl_certificate /var/www/devly.co/cert/ssl-bundle.crt;
        ssl_certificate_key /var/www/devly.co/cert/devly_co.key;



        access_log   /var/log/nginx/devly.co.access.log rt_cache;
        error_log    /var/log/nginx/devly.co.error.log;
        root /var/www/devly.co/htdocs;
        index index.php index.htm index.html;

# force https-redirects
    if ($scheme = http) {
        return 301 https://$server_name$request_uri;
}


}
0

This, works for me

server {
    listen 80;
    server_name www.example.com;
    return 301 https://example.com$request_uri;
}

server {
    listen 80;
    listen 443 ssl;
    server_name example.com;

    #location

}

p.s. with other solutions I got: ERR_TOO_MANY_REDIRECTS

1
0

First, redirect all http [80] requests to https permanently [301] from the first server block. Configure SSL in the second server block with the same domain and subdomain.

server {
    listen 80;
    listen [::]:80;
    server_name domain.com www.domain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    root /var/www/html/app;

    server_name domain.com www.domain.com;

    ssl_certificate /etc/ssl/chain.crt;
    ssl_certificate_key /etc/ssl/private.key;
}
1
  • This doesn’t answer the “remove www” part of the question. This only redirects from clear text http to https
    – HBruijn
    Jul 25, 2023 at 6:49

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .